@linktr.ee/messaging-react 3.38.2 → 4.0.0-rc-1786383199

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 (32) hide show
  1. package/dist/{Card-2Ms7lBDO.js → Card-B3XsV8Td.js} +2 -2
  2. package/dist/{Card-2Ms7lBDO.js.map → Card-B3XsV8Td.js.map} +1 -1
  3. package/dist/{Card-KwVE3M5j.cjs → Card-DkQdjw4V.cjs} +2 -2
  4. package/dist/{Card-KwVE3M5j.cjs.map → Card-DkQdjw4V.cjs.map} +1 -1
  5. package/dist/index-B1u9TBSD.cjs +3 -0
  6. package/dist/index-B1u9TBSD.cjs.map +1 -0
  7. package/dist/{index-i03XGFsr.js → index-NfAAanC-.js} +372 -371
  8. package/dist/index-NfAAanC-.js.map +1 -0
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.d.ts +18 -13
  11. package/dist/index.js +1 -1
  12. package/dist/testing.d.ts +5 -2
  13. package/package.json +2 -2
  14. package/src/components/{ChannelHeaderRedesign.tsx → ChannelHeader.tsx} +4 -2
  15. package/src/components/{ChannelView.broadcast-name.integration.test.tsx → ChannelView.broadcast-resolution.integration.test.tsx} +11 -9
  16. package/src/components/ChannelView.tsx +15 -15
  17. package/src/components/CustomMessage/BroadcastContext.test.ts +52 -0
  18. package/src/components/CustomMessage/BroadcastContext.ts +101 -0
  19. package/src/components/CustomMessage/CustomMessage.stories.tsx +29 -21
  20. package/src/components/CustomMessage/CustomMessage.test.tsx +49 -20
  21. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +37 -17
  22. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +17 -24
  23. package/src/components/CustomMessage/index.tsx +18 -13
  24. package/src/components/MessagingShell/index.tsx +2 -2
  25. package/src/index.ts +2 -1
  26. package/src/stream-custom-data.ts +5 -2
  27. package/src/types.ts +13 -9
  28. package/dist/index-DzOGH7lG.cjs +0 -3
  29. package/dist/index-DzOGH7lG.cjs.map +0 -1
  30. package/dist/index-i03XGFsr.js.map +0 -1
  31. package/src/components/CustomMessage/BroadcastNameContext.test.ts +0 -43
  32. package/src/components/CustomMessage/BroadcastNameContext.ts +0 -95
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DzOGH7lG.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.MessageBubble=e.MessageBubble;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-B1u9TBSD.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.MessageBubble=e.MessageBubble;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;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.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;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.useMessaging=e.useMessaging;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -85,12 +85,9 @@ export declare interface AvatarProps {
85
85
  dmAgentEnabled?: boolean;
86
86
  }
87
87
 
88
- export declare type BroadcastClickHandler = (broadcast: {
89
- id: string;
90
- name?: string;
91
- }) => void;
88
+ export declare type BroadcastClickHandler = (broadcast: ResolvedBroadcast) => void;
92
89
 
93
- export declare type BroadcastNameResolver = (broadcastId: string) => string | undefined;
90
+ export declare type BroadcastResolver = (outboundId: string) => ResolvedBroadcast | undefined;
94
91
 
95
92
  /**
96
93
  * Build the meta line shown under the title in compact document / file
@@ -215,7 +212,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
215
212
  * Props that MessagingShell passes through to ChannelView.
216
213
  * ChannelViewProps is the source of truth for these props.
217
214
  */
218
- declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'resolveBroadcastName' | 'showChannelInfo' | 'composerInput'>;
215
+ declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'viewerLanguage' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'onBroadcastClick' | 'resolveBroadcast' | 'showChannelInfo' | 'composerInput'>;
219
216
 
220
217
  /**
221
218
  * ChannelView component props
@@ -400,12 +397,12 @@ export declare interface ChannelViewProps {
400
397
  */
401
398
  onBroadcastClick?: BroadcastClickHandler;
402
399
  /**
403
- * Resolves the current campaign name for a broadcast label. The host must
404
- * provide a new resolver reference when a returned name changes so
405
- * ChannelView re-renders and publishes the new value to memoized rows. The
406
- * stamped message name remains the fallback until this returns a name.
400
+ * Resolves an outbound record id to the current broadcast id and name. The
401
+ * host must provide a new resolver reference when a returned broadcast
402
+ * changes so ChannelView re-renders and publishes the new value to memoized
403
+ * rows. Missing records stay unresolved and render no label.
407
404
  */
408
- resolveBroadcastName?: BroadcastNameResolver;
405
+ resolveBroadcast?: BroadcastResolver;
409
406
  /**
410
407
  * Passed to Stream `Channel` as `SendButton`. Required for hosts that replace
411
408
  * the send control: `Channel` merges this into `ComponentContext` and an
@@ -1409,8 +1406,11 @@ export declare interface MessageMetadata {
1409
1406
  * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
1410
1407
  */
1411
1408
  sent_from?: 'stack_detail';
1412
- broadcast_id?: string;
1413
- broadcast_name?: string;
1409
+ /**
1410
+ * Opaque Outbound Record id on automation broadcast messages. Hosts resolve
1411
+ * this server-side to the current automation id and name.
1412
+ */
1413
+ outbound_id?: string;
1414
1414
  }
1415
1415
 
1416
1416
  declare type MessageWithI18n = Pick<LocalMessage, 'text'> & {
@@ -1635,6 +1635,11 @@ export declare interface ReceivedBubbleProps extends MessageBubbleBaseProps {
1635
1635
  */
1636
1636
  export declare function resolveConversationParticipant(members: ChannelMembers | undefined, currentUserId: string | undefined, channelType: string | undefined): ChannelMember | undefined;
1637
1637
 
1638
+ export declare type ResolvedBroadcast = {
1639
+ id: string;
1640
+ name?: string;
1641
+ };
1642
+
1638
1643
  export declare function resolveLinkAttachment(message: LocalMessage): Attachment | undefined;
1639
1644
 
1640
1645
  export declare function resolveMediaFromMessage(message: LocalMessage): MediaMessageResolved | null;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-i03XGFsr.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-NfAAanC-.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/dist/testing.d.ts CHANGED
@@ -56,8 +56,11 @@ export declare interface MessageMetadata {
56
56
  * `custom_type` — it identifies the send context, not the message kind. (MES-1266)
57
57
  */
58
58
  sent_from?: 'stack_detail';
59
- broadcast_id?: string;
60
- broadcast_name?: string;
59
+ /**
60
+ * Opaque Outbound Record id on automation broadcast messages. Hosts resolve
61
+ * this server-side to the current automation id and name.
62
+ */
63
+ outbound_id?: string;
61
64
  }
62
65
 
63
66
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "3.38.2",
3
+ "version": "4.0.0-rc-1786383199",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "^2.4.0",
53
+ "@linktr.ee/messaging-core": "2.4.0-rc-1786383199",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.9.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
@@ -28,7 +28,7 @@ const isCustomerPaidTag = (value: unknown) =>
28
28
  .replace(/[\s-]+/g, '_')
29
29
  .toUpperCase() === CUSTOMER_PAID_TAG
30
30
 
31
- interface ChannelHeaderRedesignProps {
31
+ interface ChannelHeaderProps {
32
32
  className?: string
33
33
  onBack?: () => void
34
34
  showBackButton: boolean
@@ -47,7 +47,7 @@ interface ChannelHeaderRedesignProps {
47
47
  onParticipantNameClick?: () => void
48
48
  }
49
49
 
50
- export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
50
+ const ChannelHeader: React.FC<ChannelHeaderProps> = ({
51
51
  className,
52
52
  onBack,
53
53
  showBackButton,
@@ -241,3 +241,5 @@ export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
241
241
  </div>
242
242
  )
243
243
  }
244
+
245
+ export default ChannelHeader
@@ -14,8 +14,7 @@ const message = {
14
14
  status: 'received',
15
15
  user: { id: 'linker-1', name: 'Linker' },
16
16
  metadata: {
17
- broadcast_id: 'broadcast-1',
18
- broadcast_name: 'July 9th Class attendees',
17
+ outbound_id: 'outbound-1',
19
18
  },
20
19
  attachments: [],
21
20
  latest_reactions: [],
@@ -54,11 +53,14 @@ const createChannel = () => {
54
53
  return { channel, client }
55
54
  }
56
55
 
57
- describe('ChannelView broadcast-name resolution', () => {
58
- it('updates an existing Stream message row when the resolved name arrives', async () => {
56
+ describe('ChannelView broadcast resolution', () => {
57
+ it('updates an existing Stream message row when its outbound lookup resolves', async () => {
59
58
  const { channel, client } = createChannel()
60
59
  const firstResolver = () => undefined
61
- const secondResolver = () => 'July 9th Class absentee'
60
+ const secondResolver = () => ({
61
+ id: 'broadcast-1',
62
+ name: 'July 9th Class absentee',
63
+ })
62
64
  const mounts = vi.fn()
63
65
  const unmounts = vi.fn()
64
66
 
@@ -80,7 +82,7 @@ describe('ChannelView broadcast-name resolution', () => {
80
82
  <Channel channel={channel as StreamChannel}>
81
83
  <ChannelView
82
84
  channel={channel as StreamChannel}
83
- resolveBroadcastName={firstResolver}
85
+ resolveBroadcast={firstResolver}
84
86
  renderMessage={renderMessage}
85
87
  />
86
88
  </Channel>
@@ -89,8 +91,8 @@ describe('ChannelView broadcast-name resolution', () => {
89
91
  })
90
92
 
91
93
  expect(
92
- screen.getByTestId('sent-message-broadcast-status')
93
- ).toHaveTextContent('July 9th Class attendees')
94
+ screen.queryByTestId('sent-message-broadcast-status')
95
+ ).not.toBeInTheDocument()
94
96
  expect(mounts).toHaveBeenCalledOnce()
95
97
 
96
98
  act(() => {
@@ -99,7 +101,7 @@ describe('ChannelView broadcast-name resolution', () => {
99
101
  <Channel channel={channel as StreamChannel}>
100
102
  <ChannelView
101
103
  channel={channel as StreamChannel}
102
- resolveBroadcastName={secondResolver}
104
+ resolveBroadcast={secondResolver}
103
105
  renderMessage={renderMessage}
104
106
  />
105
107
  </Channel>
@@ -13,13 +13,13 @@ import {
13
13
 
14
14
  import type { ChannelViewProps } from '../types'
15
15
 
16
- import { ChannelHeaderRedesign } from './ChannelHeaderRedesign'
16
+ import ChannelHeader from './ChannelHeader'
17
17
  import { CustomDateSeparator } from './CustomDateSeparator'
18
18
  import { CustomMessage } from './CustomMessage'
19
19
  import {
20
- BroadcastNameContext,
21
- useBroadcastNamesById,
22
- } from './CustomMessage/BroadcastNameContext'
20
+ BroadcastContext,
21
+ useBroadcastsByOutboundId,
22
+ } from './CustomMessage/BroadcastContext'
23
23
  import { CustomMessageActions } from './CustomMessage/CustomMessageActions'
24
24
  import { CustomMessageInput } from './CustomMessageInput'
25
25
  import { CustomSystemMessage } from './CustomSystemMessage'
@@ -60,7 +60,7 @@ const ChannelViewInner: React.FC<{
60
60
  showChannelInfo?: boolean
61
61
  onParticipantNameClick?: () => void
62
62
  onBroadcastClick?: ChannelViewProps['onBroadcastClick']
63
- resolveBroadcastName?: ChannelViewProps['resolveBroadcastName']
63
+ resolveBroadcast?: ChannelViewProps['resolveBroadcast']
64
64
  composerInput?: ChannelViewProps['composerInput']
65
65
  }> = ({
66
66
  onBack,
@@ -87,7 +87,7 @@ const ChannelViewInner: React.FC<{
87
87
  showChannelInfo = true,
88
88
  onParticipantNameClick,
89
89
  onBroadcastClick,
90
- resolveBroadcastName,
90
+ resolveBroadcast,
91
91
  composerInput,
92
92
  }) => {
93
93
  const { channel, messages } = useChannelStateContext()
@@ -103,11 +103,11 @@ const ChannelViewInner: React.FC<{
103
103
  const messageBroadcastClick = onBroadcastClick
104
104
  ? stableOnBroadcastClick
105
105
  : undefined
106
- // Resolve outside memoized Stream rows so new campaign names publish without
107
- // remounting the rows.
108
- const broadcastNamesById = useBroadcastNamesById(
106
+ // Resolve outside memoized Stream rows so current campaign details publish
107
+ // without remounting the rows.
108
+ const broadcastsByOutboundId = useBroadcastsByOutboundId(
109
109
  messages,
110
- resolveBroadcastName
110
+ resolveBroadcast
111
111
  )
112
112
 
113
113
  const hasVisibleMessages = channel.state.messages.some(
@@ -119,7 +119,7 @@ const ChannelViewInner: React.FC<{
119
119
  </React.Fragment>
120
120
  ) : null
121
121
  const channelHeader = (
122
- <ChannelHeaderRedesign
122
+ <ChannelHeader
123
123
  className="-mx-[var(--str-chat\_\_spacing-2)] px-[var(--str-chat\_\_spacing-2)] md:-mx-[min(var(--str-chat\_\_spacing-10),4%)] md:px-[min(var(--str-chat\_\_spacing-10),4%)]"
124
124
  onBack={onBack}
125
125
  showBackButton={showBackButton}
@@ -162,7 +162,7 @@ const ChannelViewInner: React.FC<{
162
162
  )
163
163
 
164
164
  return (
165
- <BroadcastNameContext.Provider value={broadcastNamesById}>
165
+ <BroadcastContext.Provider value={broadcastsByOutboundId}>
166
166
  <WithComponents
167
167
  overrides={{
168
168
  Message: MessageOverride,
@@ -274,7 +274,7 @@ const ChannelViewInner: React.FC<{
274
274
  )}
275
275
  </Window>
276
276
  </WithComponents>
277
- </BroadcastNameContext.Provider>
277
+ </BroadcastContext.Provider>
278
278
  )
279
279
  }
280
280
 
@@ -316,7 +316,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
316
316
  showChannelInfo = true,
317
317
  onParticipantNameClick,
318
318
  onBroadcastClick,
319
- resolveBroadcastName,
319
+ resolveBroadcast,
320
320
  composerInput,
321
321
  }) => {
322
322
  // Custom send message handler that:
@@ -430,7 +430,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
430
430
  showChannelInfo={showChannelInfo}
431
431
  onParticipantNameClick={onParticipantNameClick}
432
432
  onBroadcastClick={onBroadcastClick}
433
- resolveBroadcastName={resolveBroadcastName}
433
+ resolveBroadcast={resolveBroadcast}
434
434
  composerInput={composerInput}
435
435
  />
436
436
  </Channel>
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { buildBroadcastsByOutboundId } from './BroadcastContext'
4
+
5
+ describe('buildBroadcastsByOutboundId', () => {
6
+ it('returns undefined when no resolver is provided', () => {
7
+ expect(
8
+ buildBroadcastsByOutboundId(
9
+ [{ metadata: { outbound_id: 'outbound-1' } }],
10
+ undefined
11
+ )
12
+ ).toBeUndefined()
13
+ })
14
+
15
+ it('resolves each unique outbound id once', () => {
16
+ const resolveBroadcast = vi.fn((outboundId: string) => ({
17
+ id: `broadcast-for-${outboundId}`,
18
+ name: `name-for-${outboundId}`,
19
+ }))
20
+
21
+ expect(
22
+ buildBroadcastsByOutboundId(
23
+ [
24
+ { metadata: { outbound_id: 'outbound-1' } },
25
+ { metadata: { outbound_id: 'outbound-1' } },
26
+ { metadata: { outbound_id: 'outbound-2' } },
27
+ { metadata: {} },
28
+ ],
29
+ resolveBroadcast
30
+ )
31
+ ).toEqual({
32
+ 'outbound-1': {
33
+ id: 'broadcast-for-outbound-1',
34
+ name: 'name-for-outbound-1',
35
+ },
36
+ 'outbound-2': {
37
+ id: 'broadcast-for-outbound-2',
38
+ name: 'name-for-outbound-2',
39
+ },
40
+ })
41
+ expect(resolveBroadcast).toHaveBeenCalledTimes(2)
42
+ })
43
+
44
+ it('omits outbound records the resolver cannot resolve', () => {
45
+ expect(
46
+ buildBroadcastsByOutboundId(
47
+ [{ metadata: { outbound_id: 'outbound-1' } }],
48
+ () => undefined
49
+ )
50
+ ).toBeUndefined()
51
+ })
52
+ })
@@ -0,0 +1,101 @@
1
+ import { createContext, useState } from 'react'
2
+
3
+ import type { BroadcastResolver, ResolvedBroadcast } from '../../types'
4
+
5
+ /** Current broadcasts keyed by the outbound record id stored on each message. */
6
+ export type BroadcastsByOutboundId = Readonly<
7
+ Record<string, ResolvedBroadcast>
8
+ >
9
+
10
+ export const BroadcastContext = createContext<
11
+ BroadcastsByOutboundId | undefined
12
+ >(undefined)
13
+
14
+ const outboundIdFromMessage = (message: {
15
+ metadata?: { outbound_id?: unknown }
16
+ }): string | undefined => {
17
+ const outboundId = message.metadata?.outbound_id
18
+ return typeof outboundId === 'string' ? outboundId : undefined
19
+ }
20
+
21
+ /**
22
+ * Resolve broadcasts for the messages currently in view.
23
+ *
24
+ * Built outside memoized Stream rows so a new resolver reference can publish
25
+ * current automation details without remounting the Stream message rows.
26
+ */
27
+ export const buildBroadcastsByOutboundId = (
28
+ messages: readonly { metadata?: { outbound_id?: unknown } }[] | undefined,
29
+ resolveBroadcast: BroadcastResolver | undefined
30
+ ): BroadcastsByOutboundId | undefined => {
31
+ if (!resolveBroadcast || !messages?.length) {
32
+ return undefined
33
+ }
34
+
35
+ const seen = new Set<string>()
36
+ let broadcasts: Record<string, ResolvedBroadcast> | undefined
37
+
38
+ for (const message of messages) {
39
+ const outboundId = outboundIdFromMessage(message)
40
+ if (!outboundId || seen.has(outboundId)) {
41
+ continue
42
+ }
43
+ seen.add(outboundId)
44
+
45
+ const resolved = resolveBroadcast(outboundId)
46
+ if (!resolved) {
47
+ continue
48
+ }
49
+
50
+ broadcasts ??= {}
51
+ broadcasts[outboundId] = resolved
52
+ }
53
+
54
+ return broadcasts
55
+ }
56
+
57
+ const areBroadcastsEqual = (
58
+ previous: BroadcastsByOutboundId | undefined,
59
+ next: BroadcastsByOutboundId | undefined
60
+ ): boolean => {
61
+ if (previous === next) {
62
+ return true
63
+ }
64
+ if (!previous || !next) {
65
+ return false
66
+ }
67
+
68
+ const previousOutboundIds = Object.keys(previous)
69
+ const nextOutboundIds = Object.keys(next)
70
+ if (previousOutboundIds.length !== nextOutboundIds.length) {
71
+ return false
72
+ }
73
+
74
+ return previousOutboundIds.every(
75
+ (outboundId) =>
76
+ previous[outboundId].id === next[outboundId]?.id &&
77
+ previous[outboundId].name === next[outboundId]?.name
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Re-invokes the resolver on every render, but only returns a new object when
83
+ * the resolved id/name pairs actually change — so context consumers update for
84
+ * late-arriving broadcasts without thrashing on unrelated renders.
85
+ */
86
+ export const useBroadcastsByOutboundId = (
87
+ messages: readonly { metadata?: { outbound_id?: unknown } }[] | undefined,
88
+ resolveBroadcast: BroadcastResolver | undefined
89
+ ): BroadcastsByOutboundId | undefined => {
90
+ const broadcasts = buildBroadcastsByOutboundId(messages, resolveBroadcast)
91
+ const [publishedBroadcasts, setPublishedBroadcasts] = useState<
92
+ BroadcastsByOutboundId | undefined
93
+ >(broadcasts)
94
+
95
+ if (!areBroadcastsEqual(publishedBroadcasts, broadcasts)) {
96
+ setPublishedBroadcasts(broadcasts)
97
+ return broadcasts
98
+ }
99
+
100
+ return publishedBroadcasts
101
+ }
@@ -23,13 +23,13 @@ import {
23
23
  storyUsers,
24
24
  } from '../../stories/decorators/storyUser'
25
25
  import { createMockStreamChatClient } from '../../testing/createMockStreamChatClient'
26
- import type { BroadcastNameResolver } from '../../types'
26
+ import type { BroadcastResolver } from '../../types'
27
27
  import CustomTypingIndicator from '../CustomTypingIndicator'
28
28
 
29
29
  import {
30
- BroadcastNameContext,
31
- buildBroadcastNamesById,
32
- } from './BroadcastNameContext'
30
+ BroadcastContext,
31
+ buildBroadcastsByOutboundId,
32
+ } from './BroadcastContext'
33
33
  import { CustomMessageActions } from './CustomMessageActions'
34
34
 
35
35
  import { CustomMessage } from './index'
@@ -112,7 +112,7 @@ const createMockChannel = async (
112
112
  interface TemplateProps {
113
113
  currentUser: StoryUser
114
114
  typingUser?: StoryUser
115
- resolveBroadcastName?: BroadcastNameResolver
115
+ resolveBroadcast?: BroadcastResolver
116
116
  messages: Array<{
117
117
  id: string
118
118
  text: string
@@ -134,8 +134,7 @@ interface TemplateProps {
134
134
  attachment_thumbnail?: string
135
135
  attachment_detail?: string
136
136
  attachment_content_type?: 'text' | 'media'
137
- broadcast_id?: string
138
- broadcast_name?: string
137
+ outbound_id?: string
139
138
  }
140
139
  }>
141
140
  }
@@ -144,8 +143,8 @@ const TemplateInner: React.FC<{
144
143
  currentUser: StoryUser
145
144
  typingUser?: StoryUser
146
145
  messages: TemplateProps['messages']
147
- resolveBroadcastName?: BroadcastNameResolver
148
- }> = ({ currentUser, typingUser, messages, resolveBroadcastName }) => {
146
+ resolveBroadcast?: BroadcastResolver
147
+ }> = ({ currentUser, typingUser, messages, resolveBroadcast }) => {
149
148
  const [client] = React.useState(() => createMockStreamChatClient(currentUser))
150
149
 
151
150
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
@@ -171,9 +170,9 @@ const TemplateInner: React.FC<{
171
170
  return () => clearTimeout(timer)
172
171
  }, [channel, client, typingUser])
173
172
 
174
- const broadcastNamesById = React.useMemo(
175
- () => buildBroadcastNamesById(messages, resolveBroadcastName),
176
- [messages, resolveBroadcastName]
173
+ const broadcastsByOutboundId = React.useMemo(
174
+ () => buildBroadcastsByOutboundId(messages, resolveBroadcast),
175
+ [messages, resolveBroadcast]
177
176
  )
178
177
 
179
178
  const MessageComponent = React.useMemo(() => {
@@ -187,7 +186,7 @@ const TemplateInner: React.FC<{
187
186
  }
188
187
 
189
188
  return (
190
- <BroadcastNameContext.Provider value={broadcastNamesById}>
189
+ <BroadcastContext.Provider value={broadcastsByOutboundId}>
191
190
  <Chat client={client}>
192
191
  <div className="h-screen w-full bg-white">
193
192
  <Channel
@@ -201,7 +200,7 @@ const TemplateInner: React.FC<{
201
200
  </Channel>
202
201
  </div>
203
202
  </Chat>
204
- </BroadcastNameContext.Provider>
203
+ </BroadcastContext.Provider>
205
204
  )
206
205
  }
207
206
 
@@ -209,14 +208,14 @@ const Template: StoryFn<TemplateProps> = ({
209
208
  currentUser = storyUsers.creator,
210
209
  typingUser,
211
210
  messages,
212
- resolveBroadcastName,
211
+ resolveBroadcast,
213
212
  }) => (
214
213
  <TemplateInner
215
214
  key={currentUser.id}
216
215
  currentUser={currentUser}
217
216
  typingUser={typingUser}
218
217
  messages={messages}
219
- resolveBroadcastName={resolveBroadcastName}
218
+ resolveBroadcast={resolveBroadcast}
220
219
  />
221
220
  )
222
221
 
@@ -303,11 +302,14 @@ BroadcastStatus.args = {
303
302
  user: storyUsers.creator,
304
303
  metadata: {
305
304
  custom_type: 'MESSAGE_OUTBOUND',
306
- broadcast_id: 'broadcast-1',
307
- broadcast_name: 'July 9th Class attendees',
305
+ outbound_id: 'outbound-1',
308
306
  },
309
307
  },
310
308
  ],
309
+ resolveBroadcast: () => ({
310
+ id: 'broadcast-1',
311
+ name: 'July 9th Class attendees',
312
+ }),
311
313
  }
312
314
 
313
315
  export const BroadcastStatusNonLatest: StoryFn<TemplateProps> = Template.bind(
@@ -321,8 +323,7 @@ BroadcastStatusNonLatest.args = {
321
323
  user: storyUsers.creator,
322
324
  metadata: {
323
325
  custom_type: 'MESSAGE_OUTBOUND',
324
- broadcast_id: 'broadcast-1',
325
- broadcast_name: 'July 9th Class attendees',
326
+ outbound_id: 'outbound-1',
326
327
  },
327
328
  },
328
329
  {
@@ -331,12 +332,19 @@ BroadcastStatusNonLatest.args = {
331
332
  user: storyUsers.creator,
332
333
  },
333
334
  ],
335
+ resolveBroadcast: () => ({
336
+ id: 'broadcast-1',
337
+ name: 'July 9th Class attendees',
338
+ }),
334
339
  }
335
340
 
336
341
  export const BroadcastStatusResolved: StoryFn<TemplateProps> = Template.bind({})
337
342
  BroadcastStatusResolved.args = {
338
343
  ...BroadcastStatus.args,
339
- resolveBroadcastName: () => 'July 9th Class absentee',
344
+ resolveBroadcast: () => ({
345
+ id: 'broadcast-1',
346
+ name: 'July 9th Class absentee',
347
+ }),
340
348
  }
341
349
 
342
350
  export const WithTipTag: StoryFn<TemplateProps> = Template.bind({})