@linktr.ee/messaging-react 3.6.2-rc-1783305830 → 3.6.2-rc-1783311170

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 (31) hide show
  1. package/dist/{Card-DzbA42LS.js → Card-CJh6R76n.js} +2 -2
  2. package/dist/{Card-DzbA42LS.js.map → Card-CJh6R76n.js.map} +1 -1
  3. package/dist/{Card-ApKu84eU.cjs → Card-CK1jw1VU.cjs} +2 -2
  4. package/dist/{Card-ApKu84eU.cjs.map → Card-CK1jw1VU.cjs.map} +1 -1
  5. package/dist/{Card-BsrjOcey.cjs → Card-CSwQT3wz.cjs} +2 -2
  6. package/dist/{Card-BsrjOcey.cjs.map → Card-CSwQT3wz.cjs.map} +1 -1
  7. package/dist/{Card-DRsxhw3X.cjs → Card-DQtg7wWP.cjs} +2 -2
  8. package/dist/{Card-DRsxhw3X.cjs.map → Card-DQtg7wWP.cjs.map} +1 -1
  9. package/dist/{Card-ByFl70ay.js → Card-DUoHKAuh.js} +2 -2
  10. package/dist/{Card-ByFl70ay.js.map → Card-DUoHKAuh.js.map} +1 -1
  11. package/dist/{Card-BQaLggg8.js → Card-DnEyeR_d.js} +3 -3
  12. package/dist/{Card-BQaLggg8.js.map → Card-DnEyeR_d.js.map} +1 -1
  13. package/dist/{LockedThumbnail-C6roO49v.cjs → LockedThumbnail-BZLpmTpY.cjs} +2 -2
  14. package/dist/{LockedThumbnail-C6roO49v.cjs.map → LockedThumbnail-BZLpmTpY.cjs.map} +1 -1
  15. package/dist/{LockedThumbnail-B1MyKjeC.js → LockedThumbnail-C_F2Sw-9.js} +2 -2
  16. package/dist/{LockedThumbnail-B1MyKjeC.js.map → LockedThumbnail-C_F2Sw-9.js.map} +1 -1
  17. package/dist/assets/index.css +1 -1
  18. package/dist/{index-Dy-mY8YE.js → index-DcEVHNS3.js} +1062 -1068
  19. package/dist/index-DcEVHNS3.js.map +1 -0
  20. package/dist/index-LP0PQJxl.cjs +2 -0
  21. package/dist/index-LP0PQJxl.cjs.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/SentMessageDeliveryStatus.test.tsx +64 -53
  26. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +54 -36
  27. package/src/components/CustomMessage/index.tsx +7 -10
  28. package/src/styles.css +19 -4
  29. package/dist/index-Cx5L9rqX.cjs +0 -2
  30. package/dist/index-Cx5L9rqX.cjs.map +0 -1
  31. package/dist/index-Dy-mY8YE.js.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-Cx5L9rqX.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-LP0PQJxl.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-Dy-mY8YE.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-DcEVHNS3.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.2-rc-1783305830",
3
+ "version": "3.6.2-rc-1783311170",
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.3.0-rc-1783305830",
41
+ "@linktr.ee/messaging-core": "2.3.0-rc-1783311170",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -1,72 +1,83 @@
1
- import type { LocalMessage } from 'stream-chat'
2
- import { describe, expect, it } from 'vitest'
1
+ import React from 'react'
2
+ import { useMessageContext } from 'stream-chat-react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
4
 
4
- import { renderWithProviders, screen } from '../../test/utils'
5
+ import { renderWithProviders, screen, within } from '../../test/utils'
5
6
 
6
7
  import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
7
8
 
8
- const baseMessage = {
9
- id: 'message-1',
10
- status: 'received',
11
- type: 'regular',
12
- } as unknown as LocalMessage
9
+ // stream-chat-react's `MessageStatus` owns the outer gating (own message, not
10
+ // error, not in a thread) and the state machine — Stream's tested code. What
11
+ // this component owns is the "Delivered" label wired into the terminal slots
12
+ // (sent/delivered/read), its "last own message" self-gate, and suppressing
13
+ // the sending spinner. Mock `MessageStatus` to render the slots it was handed,
14
+ // and `useMessageContext` to feed the label's self-gate.
15
+ vi.mock('stream-chat-react', () => ({
16
+ useMessageContext: vi.fn(),
17
+ MessageStatus: ({
18
+ MessageSentStatus,
19
+ MessageDeliveredStatus,
20
+ MessageReadStatus,
21
+ MessageSendingStatus,
22
+ }: Record<string, React.ComponentType>) => (
23
+ <div>
24
+ <div data-testid="sent-slot">
25
+ <MessageSentStatus />
26
+ </div>
27
+ <div data-testid="delivered-slot">
28
+ <MessageDeliveredStatus />
29
+ </div>
30
+ <div data-testid="read-slot">
31
+ <MessageReadStatus />
32
+ </div>
33
+ <div data-testid="sending-slot">
34
+ <MessageSendingStatus />
35
+ </div>
36
+ </div>
37
+ ),
38
+ }))
13
39
 
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
- )
40
+ const mockedUseMessageContext = vi.mocked(useMessageContext)
28
41
 
29
- describe('SentMessageDeliveryStatus', () => {
30
- it('renders "Delivered" for the last own message once Stream has received it', () => {
31
- renderStatus()
42
+ const TERMINAL_SLOTS = ['sent-slot', 'delivered-slot', 'read-slot'] as const
32
43
 
33
- expect(
34
- screen.getByTestId('sent-message-delivery-status')
35
- ).toBeInTheDocument()
36
- expect(screen.getByText(/delivered/i)).toBeInTheDocument()
44
+ describe('SentMessageDeliveryStatus', () => {
45
+ beforeEach(() => {
46
+ mockedUseMessageContext.mockReturnValue({
47
+ lastOwnMessage: { id: 'message-1' },
48
+ message: { id: 'message-1' },
49
+ } as never)
37
50
  })
38
51
 
39
- it('renders nothing when the message is not authored by the viewer', () => {
40
- const { container } = renderStatus({ isMyMessage: false })
52
+ it('renders "Delivered" in every terminal slot for the last own message', () => {
53
+ // delivered/read fire ahead of sent in receipt channels, so all three
54
+ // must show the label — never Stream's default delivered icon / read
55
+ // avatars.
56
+ renderWithProviders(<SentMessageDeliveryStatus />)
41
57
 
42
- expect(container).toBeEmptyDOMElement()
58
+ for (const slot of TERMINAL_SLOTS) {
59
+ expect(
60
+ within(screen.getByTestId(slot)).getByText('Delivered')
61
+ ).toBeInTheDocument()
62
+ }
43
63
  })
44
64
 
45
- it('renders nothing when this is not the last own message', () => {
46
- const { container } = renderStatus({ lastOwnMessageId: 'message-2' })
65
+ it('renders nothing in the terminal slots when it is not the last own message', () => {
66
+ mockedUseMessageContext.mockReturnValue({
67
+ lastOwnMessage: { id: 'message-2' },
68
+ message: { id: 'message-1' },
69
+ } as never)
47
70
 
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
- })
71
+ renderWithProviders(<SentMessageDeliveryStatus />)
57
72
 
58
- expect(container).toBeEmptyDOMElement()
73
+ for (const slot of TERMINAL_SLOTS) {
74
+ expect(screen.getByTestId(slot)).toBeEmptyDOMElement()
59
75
  }
60
- )
76
+ })
61
77
 
62
- it('renders nothing for error messages', () => {
63
- const { container } = renderStatus({
64
- message: {
65
- ...baseMessage,
66
- type: 'error',
67
- } as unknown as LocalMessage,
68
- })
78
+ it('always suppresses the sending spinner', () => {
79
+ renderWithProviders(<SentMessageDeliveryStatus />)
69
80
 
70
- expect(container).toBeEmptyDOMElement()
81
+ expect(screen.getByTestId('sending-slot')).toBeEmptyDOMElement()
71
82
  })
72
83
  })
@@ -1,49 +1,67 @@
1
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
- }
2
+ import { MessageStatus, useMessageContext } from 'stream-chat-react'
11
3
 
12
4
  /**
13
- * "✓✓ Delivered" indicator shown beneath the viewer's most recent **sent**
14
- * message, right-aligned (MES-1036).
5
+ * "✓✓ Delivered" label, wired into *every* terminal `MessageStatus` state
6
+ * (sent / delivered / read).
15
7
  *
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.
8
+ * `MessageStatus` picks its `delivered` and `read` branches ahead of `sent`
9
+ * whenever Stream has `deliveredTo` / `readBy` for the message and, unlike
10
+ * `sent`, those branches are *not* gated to the last own message. So in a
11
+ * channel that has receipts we must (a) replace Stream's default delivered
12
+ * icon / read avatars with our label — otherwise the required "Delivered"
13
+ * label disappears and an out-of-scope read receipt would show — and (b)
14
+ * re-apply the "last own message only" gate ourselves, since the
15
+ * delivered/read branches would otherwise render on every read own message.
21
16
  *
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.
17
+ * Rendered inside stream's `.str-chat__message-status` span, which supplies
18
+ * the flex layout; icon and copy are styled via that span in `styles.css`.
25
19
  */
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
20
+ const DeliveredStatus = () => {
21
+ const { lastOwnMessage, message } = useMessageContext(
22
+ 'SentMessageDeliveryStatus'
23
+ )
35
24
 
36
- if (!isLastOwnMessage || message.status !== 'received') {
25
+ if (lastOwnMessage?.id !== message.id) {
37
26
  return null
38
27
  }
39
28
 
40
29
  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>
30
+ <>
31
+ <ChecksIcon weight="bold" aria-hidden="true" />
32
+ <span data-testid="sent-message-delivery-status">Delivered</span>
33
+ </>
48
34
  )
49
35
  }
36
+
37
+ /** Suppresses a MessageStatus slot we don't want to render. */
38
+ const NoStatus = () => null
39
+
40
+ /**
41
+ * Delivery status beneath the viewer's most recent sent message (MES-1036):
42
+ * a right-aligned "✓✓ Delivered" that appears once Stream confirms the
43
+ * message is stored and updates live.
44
+ *
45
+ * Built on stream-chat-react's `MessageStatus`, which owns the gating we want
46
+ * — own message, not an error, and not inside a thread — plus the
47
+ * sending/sent/delivered/read state machine.
48
+ *
49
+ * Scope is "Delivered" only (no Sending / Read). Stream's `sent` state maps
50
+ * to "Delivered" for a backend-confirmed (`status === 'received'`) last own
51
+ * message; its `delivered`/`read` states (only populated in channels with
52
+ * per-recipient receipts) also render the same "Delivered" label via
53
+ * {@link DeliveredStatus} so we never surface a read receipt. The sending
54
+ * spinner is suppressed.
55
+ *
56
+ * `DeliveredStatus` reads `lastOwnMessage` from the message context, so
57
+ * `CustomMessage`'s memo comparator must re-render when it changes (see
58
+ * `index.tsx`) for the label to hop to a newer message.
59
+ */
60
+ export const SentMessageDeliveryStatus = () => (
61
+ <MessageStatus
62
+ MessageSentStatus={DeliveredStatus}
63
+ MessageDeliveredStatus={DeliveredStatus}
64
+ MessageReadStatus={DeliveredStatus}
65
+ MessageSendingStatus={NoStatus}
66
+ />
67
+ )
@@ -75,7 +75,6 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
75
75
  highlighted,
76
76
  isMessageAIGenerated,
77
77
  isMyMessage,
78
- lastOwnMessage,
79
78
  message,
80
79
  renderText,
81
80
  threadList,
@@ -399,11 +398,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
399
398
  )}
400
399
  </div>
401
400
  )}
402
- <SentMessageDeliveryStatus
403
- message={message}
404
- isMyMessage={isMine}
405
- lastOwnMessageId={lastOwnMessage?.id}
406
- />
401
+ <SentMessageDeliveryStatus />
407
402
  {showReplyCountButton && (
408
403
  <MessageRepliesCountButton
409
404
  onClick={handleOpenThread}
@@ -423,10 +418,12 @@ const MemoizedCustomMessage = React.memo(
423
418
  (prev, next) => {
424
419
  if (prev.chatbotVotingEnabled !== next.chatbotVotingEnabled) return false
425
420
  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.
421
+ // `lastOwnMessage` drives the "Delivered" indicator, which
422
+ // `SentMessageDeliveryStatus` derives from stream's `MessageStatus`
423
+ // (reading it off the message context inside this memoised subtree).
424
+ // `areMessageUIPropsEqual` doesn't compare it, so without this a stale
425
+ // "Delivered" would linger on the previous bubble after a newer message
426
+ // is sent — re-render when it changes.
430
427
  if (prev.lastOwnMessage?.id !== next.lastOwnMessage?.id) return false
431
428
  return areMessageUIPropsEqual(prev, next)
432
429
  }
package/src/styles.css CHANGED
@@ -417,11 +417,14 @@
417
417
  }
418
418
 
419
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 {
420
+ `SentMessageDeliveryStatus` renders it through stream's `MessageStatus`,
421
+ whose `.str-chat__message-status` span is the grid item here (right-aligned
422
+ via the outer grid's `justify-items: end`). Stream emits that span for every
423
+ own message and leaves it empty when no state is active (i.e. not the last
424
+ sent message), so `:empty` collapses those away. */
425
+ .str-chat__li .str-chat__message--me .str-chat__message-status {
422
426
  grid-area: delivery-status;
423
- display: inline-flex;
424
- align-items: center;
427
+ justify-content: flex-end;
425
428
  gap: 2px;
426
429
  padding-block-start: 2px;
427
430
  padding-inline-end: 4px;
@@ -429,6 +432,18 @@
429
432
  line-height: 1.4;
430
433
  color: var(--str-chat__text-low-emphasis-color, rgba(0, 0, 0, 0.5));
431
434
  }
435
+ .str-chat__li .str-chat__message-status:empty {
436
+ display: none;
437
+ }
438
+ /* Match the check icon to the label size/colour (overrides stream's default
439
+ per-state icon sizing and status-colour fill). */
440
+ .str-chat__li .str-chat__message--me .str-chat__message-status svg {
441
+ width: 14px;
442
+ height: 14px;
443
+ }
444
+ .str-chat__li .str-chat__message--me .str-chat__message-status svg path {
445
+ fill: currentColor;
446
+ }
432
447
 
433
448
  /* Channel list load-more button overrides */
434
449
  .str-chat__channel-list .str-chat__load-more-button__button {