@linktr.ee/messaging-react 3.6.0 → 3.6.1-rc-1783305041

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-J8f28GLS.js → Card-BF8hqtLR.js} +3 -3
  2. package/dist/{Card-J8f28GLS.js.map → Card-BF8hqtLR.js.map} +1 -1
  3. package/dist/{Card-DzT94KFC.cjs → Card-BJrIwVBx.cjs} +2 -2
  4. package/dist/{Card-DzT94KFC.cjs.map → Card-BJrIwVBx.cjs.map} +1 -1
  5. package/dist/{Card-DmjLrV64.cjs → Card-Cavq6QXu.cjs} +2 -2
  6. package/dist/{Card-DmjLrV64.cjs.map → Card-Cavq6QXu.cjs.map} +1 -1
  7. package/dist/{Card-ra-g17Ur.js → Card-CsPLNSTP.js} +2 -2
  8. package/dist/{Card-ra-g17Ur.js.map → Card-CsPLNSTP.js.map} +1 -1
  9. package/dist/{Card-Cb4yfxFK.cjs → Card-HcMDKbWd.cjs} +2 -2
  10. package/dist/{Card-Cb4yfxFK.cjs.map → Card-HcMDKbWd.cjs.map} +1 -1
  11. package/dist/{Card-COTwOzVb.js → Card-_SSVVgkv.js} +2 -2
  12. package/dist/{Card-COTwOzVb.js.map → Card-_SSVVgkv.js.map} +1 -1
  13. package/dist/{LockedThumbnail-CVq5LPXE.js → LockedThumbnail-Bqz_Fq66.js} +2 -2
  14. package/dist/{LockedThumbnail-CVq5LPXE.js.map → LockedThumbnail-Bqz_Fq66.js.map} +1 -1
  15. package/dist/{LockedThumbnail-Ds_vBmYm.cjs → LockedThumbnail-DN3UoQvw.cjs} +2 -2
  16. package/dist/{LockedThumbnail-Ds_vBmYm.cjs.map → LockedThumbnail-DN3UoQvw.cjs.map} +1 -1
  17. package/dist/assets/index.css +1 -1
  18. package/dist/index-BoB8DqqZ.cjs +2 -0
  19. package/dist/index-BoB8DqqZ.cjs.map +1 -0
  20. package/dist/{index-D1XVCH1o.js → index-C4ipGbuZ.js} +1216 -1190
  21. package/dist/index-C4ipGbuZ.js.map +1 -0
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.js +1 -1
  24. package/package.json +2 -2
  25. package/src/components/CustomMessage/CustomMessage.stories.tsx +35 -0
  26. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +72 -0
  27. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +49 -0
  28. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
  29. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +9 -3
  30. package/src/components/CustomMessage/index.tsx +18 -4
  31. package/src/styles.css +19 -1
  32. package/dist/index-D1XVCH1o.js.map +0 -1
  33. package/dist/index-hvrSZScU.cjs +0 -2
  34. package/dist/index-hvrSZScU.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-hvrSZScU.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BoB8DqqZ.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageVoteButtons=e.MessageVoteButtons;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-D1XVCH1o.js";
1
+ import { a as e, b as t, C as i, c as o, d as n, e as m, F as r, f as g, L as l, h as M, M as c, i as u, j as L, k as h, l as d, m as p, n as v, o as C, p as A, q as k, s as F, t as P, u as b, v as f, w as x, x as y, y as S, z as q, B as z, D as B, E as D, G as E } from "./index-C4ipGbuZ.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.6.0",
3
+ "version": "3.6.1-rc-1783305041",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@linktr.ee/component-library": "11.8.6",
41
- "@linktr.ee/messaging-core": "^2.2.0",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783305041",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -225,6 +225,41 @@ Default.args = {
225
225
  ],
226
226
  }
227
227
 
228
+ // MES-1036: "✓✓ Delivered" appears below only the most recent *sent*
229
+ // message. Every mock message carries status: 'received', and stream's
230
+ // MessageList derives `lastOwnMessage`, so the indicator lands on the last
231
+ // creator message ("Talk soon!") and not on the earlier sent bubble.
232
+ export const DeliveredStatus: StoryFn<TemplateProps> = Template.bind({})
233
+ DeliveredStatus.args = {
234
+ messages: [
235
+ {
236
+ id: 'msg-1',
237
+ text: 'Hey, did you get my last message?',
238
+ user: storyUsers.visitor,
239
+ },
240
+ {
241
+ id: 'msg-2',
242
+ text: 'Yes! Just replying now.',
243
+ user: storyUsers.creator,
244
+ },
245
+ {
246
+ id: 'msg-3',
247
+ text: 'Sounds good.',
248
+ user: storyUsers.visitor,
249
+ },
250
+ {
251
+ id: 'msg-4',
252
+ text: 'Let me know if you need anything else.',
253
+ user: storyUsers.creator,
254
+ },
255
+ {
256
+ id: 'msg-5',
257
+ text: 'Talk soon!',
258
+ user: storyUsers.creator,
259
+ },
260
+ ],
261
+ }
262
+
228
263
  export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})
229
264
  WithTipTag.args = {
230
265
  messages: [
@@ -0,0 +1,72 @@
1
+ import type { LocalMessage } from 'stream-chat'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
7
+
8
+ const baseMessage = {
9
+ id: 'message-1',
10
+ status: 'received',
11
+ type: 'regular',
12
+ } as unknown as LocalMessage
13
+
14
+ const renderStatus = (
15
+ props: Partial<{
16
+ message: LocalMessage
17
+ isMyMessage: boolean
18
+ lastOwnMessageId?: string
19
+ }> = {}
20
+ ) =>
21
+ renderWithProviders(
22
+ <SentMessageDeliveryStatus
23
+ message={props.message ?? baseMessage}
24
+ isMyMessage={props.isMyMessage ?? true}
25
+ lastOwnMessageId={props.lastOwnMessageId ?? 'message-1'}
26
+ />
27
+ )
28
+
29
+ describe('SentMessageDeliveryStatus', () => {
30
+ it('renders "Delivered" for the last own message once Stream has received it', () => {
31
+ renderStatus()
32
+
33
+ expect(
34
+ screen.getByTestId('sent-message-delivery-status')
35
+ ).toBeInTheDocument()
36
+ expect(screen.getByText(/delivered/i)).toBeInTheDocument()
37
+ })
38
+
39
+ it('renders nothing when the message is not authored by the viewer', () => {
40
+ const { container } = renderStatus({ isMyMessage: false })
41
+
42
+ expect(container).toBeEmptyDOMElement()
43
+ })
44
+
45
+ it('renders nothing when this is not the last own message', () => {
46
+ const { container } = renderStatus({ lastOwnMessageId: 'message-2' })
47
+
48
+ expect(container).toBeEmptyDOMElement()
49
+ })
50
+
51
+ it.each(['sending', 'failed'] as const)(
52
+ 'renders nothing while the message status is %s',
53
+ (status) => {
54
+ const { container } = renderStatus({
55
+ message: { ...baseMessage, status } as unknown as LocalMessage,
56
+ })
57
+
58
+ expect(container).toBeEmptyDOMElement()
59
+ }
60
+ )
61
+
62
+ it('renders nothing for error messages', () => {
63
+ const { container } = renderStatus({
64
+ message: {
65
+ ...baseMessage,
66
+ type: 'error',
67
+ } as unknown as LocalMessage,
68
+ })
69
+
70
+ expect(container).toBeEmptyDOMElement()
71
+ })
72
+ })
@@ -0,0 +1,49 @@
1
+ import { ChecksIcon } from '@phosphor-icons/react'
2
+ import type { LocalMessage } from 'stream-chat'
3
+
4
+ interface SentMessageDeliveryStatusProps {
5
+ message: LocalMessage
6
+ /** Result of the message context's `isMyMessage()` for this message. */
7
+ isMyMessage: boolean
8
+ /** `id` of the viewer's most recent own message, from `lastOwnMessage`. */
9
+ lastOwnMessageId?: string
10
+ }
11
+
12
+ /**
13
+ * "✓✓ Delivered" indicator shown beneath the viewer's most recent **sent**
14
+ * message, right-aligned (MES-1036).
15
+ *
16
+ * "Delivered" fires once Stream confirms the message is stored on the backend
17
+ * (`status === 'received'`) and updates live as that status flips. It renders
18
+ * only on the last own message in the thread — never on earlier bubbles, and
19
+ * never on error messages. Read receipts are intentionally out of scope per
20
+ * the ticket.
21
+ *
22
+ * The "is this the last own message?" decision comes from stream-chat-react's
23
+ * `lastOwnMessage` (via `MessageContext`); the parent must re-render this
24
+ * subtree when `lastOwnMessage` changes so the label hops to a newer message.
25
+ */
26
+ export const SentMessageDeliveryStatus = ({
27
+ message,
28
+ isMyMessage,
29
+ lastOwnMessageId,
30
+ }: SentMessageDeliveryStatusProps) => {
31
+ const isLastOwnMessage =
32
+ isMyMessage &&
33
+ message.type !== 'error' &&
34
+ lastOwnMessageId === message.id
35
+
36
+ if (!isLastOwnMessage || message.status !== 'received') {
37
+ return null
38
+ }
39
+
40
+ return (
41
+ <div
42
+ className="str-chat__message-delivery-status"
43
+ data-testid="sent-message-delivery-status"
44
+ >
45
+ <ChecksIcon size={14} weight="bold" aria-hidden="true" />
46
+ <span>Delivered</span>
47
+ </div>
48
+ )
49
+ }
@@ -5,8 +5,41 @@ import { renderWithProviders, screen } from '../../test/utils'
5
5
 
6
6
  import StreamAttachmentMessage, {
7
7
  linkCaptionDuplicatesAttachmentUrl,
8
+ linkCardPropsFromStreamAttachment,
8
9
  } from './StreamAttachmentMessage'
9
10
 
11
+ describe('linkCardPropsFromStreamAttachment', () => {
12
+ it('uses the featured layout when a thumbnail is present', () => {
13
+ expect(
14
+ linkCardPropsFromStreamAttachment({
15
+ type: 'link',
16
+ og_scrape_url: 'https://linktr.ee/someone',
17
+ image_url: 'https://cdn.example.com/thumb.jpg',
18
+ }).layout
19
+ ).toBe('featured')
20
+ })
21
+
22
+ it('uses the classic layout for a plain link with no thumbnail', () => {
23
+ expect(
24
+ linkCardPropsFromStreamAttachment({
25
+ type: 'link',
26
+ og_scrape_url: 'https://linktr.ee/someone',
27
+ }).layout
28
+ ).toBe('classic')
29
+ })
30
+
31
+ it('keeps the featured layout for a playable clip with no thumbnail so the player renders', () => {
32
+ expect(
33
+ linkCardPropsFromStreamAttachment({
34
+ type: 'link',
35
+ og_scrape_url: 'https://linktr.ee/someone',
36
+ asset_url: 'https://cdn.example.com/clip.mp3',
37
+ mime_type: 'audio/mpeg',
38
+ }).layout
39
+ ).toBe('featured')
40
+ })
41
+ })
42
+
10
43
  describe('linkCaptionDuplicatesAttachmentUrl', () => {
11
44
  it('suppresses captions that are just the link plus punctuation', () => {
12
45
  expect(
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import type { Attachment } from 'stream-chat'
3
3
 
4
4
  import LinkAttachment from '../LinkAttachment'
5
+ import { isPlayableMedia } from '../LinkAttachment/components/_shared/CardThumbnail'
5
6
  import MessageAttachment from '../MessageAttachment'
6
7
  import type { BubbleGroupPosition } from '../MessageAttachment/types'
7
8
 
@@ -62,6 +63,8 @@ export function linkCardPropsFromStreamAttachment(a: Attachment) {
62
63
  const thumbnailUrl =
63
64
  trimToUndefined((a as { image_url?: string }).image_url) ??
64
65
  trimToUndefined((a as { thumb_url?: string }).thumb_url)
66
+ const sourceUrl = trimToUndefined(a.asset_url)
67
+ const mimeType = trimToUndefined(a.mime_type)
65
68
 
66
69
  return {
67
70
  title: trimToUndefined((a as { title?: string }).title),
@@ -70,9 +73,12 @@ export function linkCardPropsFromStreamAttachment(a: Attachment) {
70
73
  thumbnailUrl,
71
74
  // Playable clip embedded in a link preview — lets LinkAttachment render its
72
75
  // native video/audio player in the hero instead of a static thumbnail.
73
- sourceUrl: trimToUndefined(a.asset_url),
74
- layout: thumbnailUrl ? 'featured' : 'classic',
75
- mimeType: trimToUndefined(a.mime_type),
76
+ sourceUrl,
77
+ layout:
78
+ thumbnailUrl || isPlayableMedia(mimeType, sourceUrl)
79
+ ? 'featured'
80
+ : 'classic',
81
+ mimeType,
76
82
  } as const
77
83
  }
78
84
 
@@ -46,6 +46,7 @@ import {
46
46
  isTipOnlyMessage,
47
47
  } from './MessageTag'
48
48
  import { MessageVoteButtons } from './MessageVoteButtons'
49
+ import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
49
50
  import StreamAttachmentMessage, {
50
51
  buildOrderedAttachmentSegments,
51
52
  } from './StreamAttachmentMessage'
@@ -74,6 +75,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
74
75
  highlighted,
75
76
  isMessageAIGenerated,
76
77
  isMyMessage,
78
+ lastOwnMessage,
77
79
  message,
78
80
  renderText,
79
81
  threadList,
@@ -213,15 +215,17 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
213
215
  isChatbot && isMine && hasRenderableAttachments
214
216
  // Route generic Stream attachments through the toolkit renderer only when it
215
217
  // can faithfully own the whole message. Quoted replies (attachments are
216
- // suppressed there), polls, and AI-streamed messages fall through to the
217
- // default branch so their Poll / StreamedMessageText / message-text handling
218
- // is preserved — the accompanying message text must never be dropped.
218
+ // suppressed there), polls, AI-streamed, and chatbot messages fall through to
219
+ // the default branch so their Poll / StreamedMessageText / message-text and
220
+ // chatbot attribution (MessageTag) handling is preserved — the accompanying
221
+ // message text and attribution must never be dropped.
219
222
  const canRenderAttachmentsInToolkit =
220
223
  hasAttachmentSegments &&
221
224
  allAttachmentsRepresented &&
222
225
  !message.quoted_message &&
223
226
  !poll &&
224
- !isAIGenerated
227
+ !isAIGenerated &&
228
+ !isChatbot
225
229
 
226
230
  return (
227
231
  <>
@@ -395,6 +399,11 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
395
399
  )}
396
400
  </div>
397
401
  )}
402
+ <SentMessageDeliveryStatus
403
+ message={message}
404
+ isMyMessage={isMine}
405
+ lastOwnMessageId={lastOwnMessage?.id}
406
+ />
398
407
  {showReplyCountButton && (
399
408
  <MessageRepliesCountButton
400
409
  onClick={handleOpenThread}
@@ -414,6 +423,11 @@ const MemoizedCustomMessage = React.memo(
414
423
  (prev, next) => {
415
424
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
416
425
  if (prev.viewerLanguage !== next.viewerLanguage) return false
426
+ // `lastOwnMessage` drives the "Delivered" indicator's placement
427
+ // (`SentMessageDeliveryStatus`). `areMessageUIPropsEqual` doesn't compare
428
+ // it, so without this a stale "Delivered" would linger on the previous
429
+ // bubble after a newer message is sent — re-render when it changes.
430
+ if (prev.lastOwnMessage?.id !== next.lastOwnMessage?.id) return false
417
431
  return areMessageUIPropsEqual(prev, next)
418
432
  }
419
433
  ) as typeof CustomMessageWithContext
package/src/styles.css CHANGED
@@ -351,12 +351,16 @@
351
351
  column-gap: 0.25rem;
352
352
  }
353
353
 
354
- /* Outer grid for me (outgoing) messages — add footer row after message */
354
+ /* Outer grid for me (outgoing) messages — footer + delivery-status rows
355
+ after message. `delivery-status` carries the "Delivered" receipt on the
356
+ last sent message (MES-1036); the row collapses to 0 height on messages
357
+ that don't render it. */
355
358
  .str-chat__li .str-chat__message--me {
356
359
  grid-template-areas:
357
360
  'message-reminder'
358
361
  'message'
359
362
  'footer'
363
+ 'delivery-status'
360
364
  'replies'
361
365
  'translation-notice'
362
366
  'custom-metadata'
@@ -412,6 +416,20 @@
412
416
  align-items: flex-end;
413
417
  }
414
418
 
419
+ /* "✓✓ Delivered" delivery status under the last sent message (MES-1036).
420
+ Right-aligned via the outer grid's `justify-items: end`. */
421
+ .str-chat__li .str-chat__message-delivery-status {
422
+ grid-area: delivery-status;
423
+ display: inline-flex;
424
+ align-items: center;
425
+ gap: 2px;
426
+ padding-block-start: 2px;
427
+ padding-inline-end: 4px;
428
+ font-size: 12px;
429
+ line-height: 1.4;
430
+ color: var(--str-chat__text-low-emphasis-color, rgba(0, 0, 0, 0.5));
431
+ }
432
+
415
433
  /* Channel list load-more button overrides */
416
434
  .str-chat__channel-list .str-chat__load-more-button__button {
417
435
  background-color: #121110;