@linktr.ee/messaging-react 3.5.6 → 3.5.7-rc-1783064389

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 (30) hide show
  1. package/dist/{Card-DYe76Xh3.cjs → Card-BY5GWs2L.cjs} +2 -2
  2. package/dist/{Card-DYe76Xh3.cjs.map → Card-BY5GWs2L.cjs.map} +1 -1
  3. package/dist/{Card-B5lE5A2D.js → Card-CpgkVykU.js} +3 -3
  4. package/dist/{Card-B5lE5A2D.js.map → Card-CpgkVykU.js.map} +1 -1
  5. package/dist/{Card-D_N5hclc.cjs → Card-CsQmLvWE.cjs} +2 -2
  6. package/dist/{Card-D_N5hclc.cjs.map → Card-CsQmLvWE.cjs.map} +1 -1
  7. package/dist/{Card-rblNjC58.js → Card-CuWniEOV.js} +2 -2
  8. package/dist/{Card-rblNjC58.js.map → Card-CuWniEOV.js.map} +1 -1
  9. package/dist/{Card-MBp3Wg6r.js → Card-DQdw5-A_.js} +2 -2
  10. package/dist/{Card-MBp3Wg6r.js.map → Card-DQdw5-A_.js.map} +1 -1
  11. package/dist/{Card-CAJnEq1K.cjs → Card-DdUewLf6.cjs} +2 -2
  12. package/dist/{Card-CAJnEq1K.cjs.map → Card-DdUewLf6.cjs.map} +1 -1
  13. package/dist/{LockedThumbnail-BqyPPVrO.js → LockedThumbnail-D5hLQ6Kz.js} +2 -2
  14. package/dist/{LockedThumbnail-BqyPPVrO.js.map → LockedThumbnail-D5hLQ6Kz.js.map} +1 -1
  15. package/dist/{LockedThumbnail-C7PpVZQ0.cjs → LockedThumbnail-s_LZkGm4.cjs} +2 -2
  16. package/dist/{LockedThumbnail-C7PpVZQ0.cjs.map → LockedThumbnail-s_LZkGm4.cjs.map} +1 -1
  17. package/dist/{index-BXzwYXLT.js → index-3vM1UOPb.js} +1738 -1557
  18. package/dist/index-3vM1UOPb.js.map +1 -0
  19. package/dist/index-BIJpO0jL.cjs +2 -0
  20. package/dist/index-BIJpO0jL.cjs.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 +65 -11
  26. package/src/components/ChannelView.test.tsx +30 -12
  27. package/src/components/ChannelView.tsx +50 -22
  28. package/dist/index-BXzwYXLT.js.map +0 -1
  29. package/dist/index-BflWZq0e.cjs +0 -2
  30. package/dist/index-BflWZq0e.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-BflWZq0e.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-BIJpO0jL.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-BXzwYXLT.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-3vM1UOPb.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.5.6",
3
+ "version": "3.5.7-rc-1783064389",
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-1783064389",
42
42
  "@phosphor-icons/react": "^2.1.10"
43
43
  },
44
44
  "devDependencies": {
@@ -0,0 +1,231 @@
1
+ import {
2
+ ArrowLeftIcon,
3
+ CaretRightIcon,
4
+ CurrencyDollarIcon,
5
+ StarIcon,
6
+ } from '@phosphor-icons/react'
7
+ import classNames from 'classnames'
8
+ import React from 'react'
9
+ import type { ChannelMemberResponse, Channel as ChannelType } from 'stream-chat'
10
+ import { useChannelStateContext, useChatContext } from 'stream-chat-react'
11
+
12
+ import { useChannelStar } from '../hooks/useChannelStar'
13
+ import type { ChannelViewProps } from '../types'
14
+ import { resolveConversationParticipant } from '../utils/resolveConversationParticipant'
15
+ import { resolveParticipantDisplayName } from '../utils/resolveParticipantDisplayName'
16
+
17
+ import { Avatar } from './Avatar'
18
+ import { ChannelActionsMenu } from './ChannelActionsMenu'
19
+
20
+ const ICON_BTN_CLASS =
21
+ 'pointer-events-auto flex size-10 flex-shrink-0 items-center justify-center rounded-full bg-white/65 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.06),0px_0px_0px_1px_rgba(0,0,0,0.04)] backdrop-blur-[40px] transition-filter duration-150 hover:brightness-95 aria-expanded:brightness-95 focus-ring'
22
+
23
+ const CUSTOMER_PAID_TAG = 'CUSTOMER_PAID'
24
+
25
+ const isCustomerPaidTag = (value: unknown) =>
26
+ typeof value === 'string' &&
27
+ value
28
+ .trim()
29
+ .replace(/[\s-]+/g, '_')
30
+ .toUpperCase() === CUSTOMER_PAID_TAG
31
+
32
+ const hasCustomerPaidTag = (participant?: ChannelMemberResponse) => {
33
+ const member = participant as
34
+ | {
35
+ customerTag?: unknown
36
+ customer_tag?: unknown
37
+ }
38
+ | undefined
39
+
40
+ return [member?.customerTag, member?.customer_tag].some(isCustomerPaidTag)
41
+ }
42
+
43
+ interface ChannelHeaderRedesignProps {
44
+ onBack?: () => void
45
+ showBackButton: boolean
46
+ showStarButton?: boolean
47
+ dmAgentEnabled?: boolean
48
+ onLeaveConversation?: (channel: ChannelType) => void
49
+ onBlockParticipant?: (participantId?: string) => void
50
+ showDeleteConversation?: boolean
51
+ showBlockParticipant?: boolean
52
+ showReportParticipant?: boolean
53
+ onDeleteConversationClick?: () => void
54
+ onBlockParticipantClick?: () => void
55
+ onReportParticipantClick?: () => void
56
+ customChannelActions?: React.ReactNode
57
+ renderChannelActions?: React.ReactNode
58
+ showChannelInfo?: boolean
59
+ onParticipantNameClick: () => void
60
+ renderHeaderTitleBadges?: ChannelViewProps['renderHeaderTitleBadges']
61
+ }
62
+
63
+ export const ChannelHeaderRedesign: React.FC<ChannelHeaderRedesignProps> = ({
64
+ onBack,
65
+ showBackButton,
66
+ showStarButton = false,
67
+ dmAgentEnabled = false,
68
+ onLeaveConversation,
69
+ onBlockParticipant,
70
+ showDeleteConversation = true,
71
+ showBlockParticipant = true,
72
+ showReportParticipant = true,
73
+ onDeleteConversationClick,
74
+ onBlockParticipantClick,
75
+ onReportParticipantClick,
76
+ customChannelActions,
77
+ renderChannelActions,
78
+ showChannelInfo = true,
79
+ onParticipantNameClick,
80
+ renderHeaderTitleBadges,
81
+ }) => {
82
+ const { channel } = useChannelStateContext()
83
+ const { client } = useChatContext()
84
+
85
+ const participant = React.useMemo(
86
+ () =>
87
+ resolveConversationParticipant(
88
+ channel.state?.members,
89
+ client?.userID,
90
+ channel.type
91
+ ),
92
+ [channel.state?.members, client?.userID, channel.type]
93
+ )
94
+
95
+ const participantName = resolveParticipantDisplayName(participant?.user)
96
+ const participantImage = participant?.user?.image
97
+ const isStarred = useChannelStar(channel)
98
+ const isPaidCustomer = hasCustomerPaidTag(participant)
99
+ const headerTitleBadges = renderHeaderTitleBadges?.({
100
+ channel,
101
+ participant,
102
+ })
103
+
104
+ const handleStarClick = async () => {
105
+ try {
106
+ if (isStarred) {
107
+ await (channel as ChannelType).unpin()
108
+ } else {
109
+ await (channel as ChannelType).pin()
110
+ }
111
+ } catch (error) {
112
+ console.error(
113
+ '[ChannelHeaderRedesign] Failed to update pinned status:',
114
+ error
115
+ )
116
+ }
117
+ }
118
+
119
+ return (
120
+ <div className="@container pointer-events-none sticky top-0 z-10 w-full bg-[linear-gradient(180deg,#FBFAF9_0%,rgba(251,250,249,0.92)_58%,rgba(251,250,249,0)_100%)]">
121
+ <div className="grid min-h-[100px] grid-cols-[40px_minmax(0,1fr)_40px] items-start pb-2 pt-5 @lg:min-h-[104px] @lg:pt-6">
122
+ <div className="flex items-start">
123
+ {showBackButton && (
124
+ <button
125
+ className={classNames(
126
+ ICON_BTN_CLASS,
127
+ 'messaging-channel-view-back-button-mobile messaging-channel-view-back-button-desktop'
128
+ )}
129
+ onClick={onBack || (() => {})}
130
+ type="button"
131
+ aria-label="Back to conversations"
132
+ >
133
+ <ArrowLeftIcon className="size-5 text-black/90" />
134
+ </button>
135
+ )}
136
+ </div>
137
+
138
+ <div className="flex min-w-0 justify-center" data-dd-privacy="mask">
139
+ <button
140
+ type="button"
141
+ onClick={onParticipantNameClick}
142
+ aria-label={`View details for ${participantName}`}
143
+ className="group pointer-events-auto flex min-w-0 max-w-full appearance-none flex-col items-center border-0 bg-transparent px-2 focus-ring focus-visible:outline-none"
144
+ >
145
+ <Avatar
146
+ id={participant?.user?.id || channel.id || 'unknown'}
147
+ name={participantName}
148
+ image={participantImage}
149
+ dmAgentEnabled={dmAgentEnabled}
150
+ shape="circle"
151
+ size={48}
152
+ className="-mb-2"
153
+ />
154
+ <span className="relative z-10 inline-flex max-w-full items-center justify-center rounded-full border border-white/10 bg-white/65 py-1.5 pl-3 pr-2 shadow-[0px_0px_0px_0px_rgba(0,0,0,0.04),0px_1px_2px_0px_rgba(0,0,0,0.04),0px_8px_32px_0px_rgba(0,0,0,0.1)] backdrop-blur-[2px] transition-colors duration-150 group-hover:bg-white/80">
155
+ <span className="min-w-0 truncate text-sm font-bold leading-[1.43] tracking-[0.28px] text-black/90">
156
+ {participantName}
157
+ </span>
158
+ <div className="ml-2 flex gap-1">
159
+ {isPaidCustomer && (
160
+ <span
161
+ aria-label="Paid customer"
162
+ className="flex size-4 shrink-0 items-center justify-center rounded-full bg-[#34C759] text-white"
163
+ >
164
+ <CurrencyDollarIcon className="size-2.5" weight="bold" />
165
+ </span>
166
+ )}
167
+ {showStarButton && isStarred && (
168
+ <span
169
+ aria-label="Starred conversation"
170
+ className="flex size-4 shrink-0 items-center justify-center rounded-full bg-[#FFD60A] text-white"
171
+ >
172
+ <StarIcon className="size-2.5" weight="fill" />
173
+ </span>
174
+ )}
175
+ {headerTitleBadges && (
176
+ <span className="shrink-0">{headerTitleBadges}</span>
177
+ )}
178
+ <CaretRightIcon
179
+ aria-hidden="true"
180
+ className="size-4 shrink-0 text-black/65"
181
+ weight="bold"
182
+ />
183
+ </div>
184
+ </span>
185
+ </button>
186
+ </div>
187
+
188
+ <div className="flex items-start justify-end gap-2">
189
+ {showStarButton && (
190
+ <button
191
+ className={ICON_BTN_CLASS}
192
+ onClick={handleStarClick}
193
+ type="button"
194
+ aria-pressed={isStarred}
195
+ aria-label={
196
+ isStarred ? 'Unstar conversation' : 'Star conversation'
197
+ }
198
+ >
199
+ <StarIcon
200
+ className={classNames('size-5', {
201
+ 'text-[#FFD60A]': isStarred,
202
+ 'text-black/90': !isStarred,
203
+ })}
204
+ weight={isStarred ? 'fill' : 'regular'}
205
+ />
206
+ </button>
207
+ )}
208
+ {showChannelInfo &&
209
+ (renderChannelActions !== undefined ? (
210
+ renderChannelActions
211
+ ) : (
212
+ <ChannelActionsMenu
213
+ channel={channel as ChannelType}
214
+ participant={participant}
215
+ showDeleteConversation={showDeleteConversation}
216
+ showBlockParticipant={showBlockParticipant}
217
+ showReportParticipant={showReportParticipant}
218
+ onLeaveConversation={onLeaveConversation}
219
+ onBlockParticipant={onBlockParticipant}
220
+ onDeleteConversationClick={onDeleteConversationClick}
221
+ onBlockParticipantClick={onBlockParticipantClick}
222
+ onReportParticipantClick={onReportParticipantClick}
223
+ customChannelActions={customChannelActions}
224
+ triggerClassName={ICON_BTN_CLASS}
225
+ />
226
+ ))}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ )
231
+ }
@@ -16,6 +16,7 @@ import { ChannelView } from './ChannelView'
16
16
  import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
17
17
 
18
18
  type ComponentProps = React.ComponentProps<typeof ChannelView>
19
+ const SCROLLABLE_MESSAGE_COUNT = 10
19
20
 
20
21
  const meta: Meta<ComponentProps> = {
21
22
  title: 'ChannelView',
@@ -41,7 +42,9 @@ const createMockChannel = async (
41
42
  client: StreamChat,
42
43
  hasMessages = true,
43
44
  followerStatus?: string | boolean,
44
- isFrozen = false
45
+ isFrozen = false,
46
+ participantCustomerTag?: string,
47
+ hasScrollableMessages = false
45
48
  ) => {
46
49
  const participant = mockParticipants[0]
47
50
 
@@ -101,6 +104,34 @@ const createMockChannel = async (
101
104
  cid: 'messaging:storybook-channel-1',
102
105
  mentioned_users: [],
103
106
  },
107
+ ...(hasScrollableMessages
108
+ ? Array.from({ length: SCROLLABLE_MESSAGE_COUNT }, (_, index) => {
109
+ const isParticipantMessage = index % 2 === 0
110
+ const user = isParticipantMessage ? participant : mockUser
111
+ const text = isParticipantMessage
112
+ ? `Here is another message to show how the redesigned header floats above the thread (${index + 1}).`
113
+ : `Replying so the conversation has enough content to scroll under the header (${index + 1}).`
114
+
115
+ return {
116
+ id: `msg-extra-${index + 1}`,
117
+ text,
118
+ type: 'regular' as const,
119
+ created_at: minutesAgo(Math.max(1, 25 - index)),
120
+ updated_at: minutesAgo(Math.max(1, 25 - index)),
121
+ user,
122
+ html: `<p>${text}</p>`,
123
+ attachments: [],
124
+ latest_reactions: [],
125
+ own_reactions: [],
126
+ reaction_counts: {},
127
+ reaction_scores: {},
128
+ reply_count: 0,
129
+ status: 'received',
130
+ cid: 'messaging:storybook-channel-1',
131
+ mentioned_users: [],
132
+ }
133
+ })
134
+ : []),
104
135
  ]
105
136
  : []
106
137
 
@@ -118,7 +149,11 @@ const createMockChannel = async (
118
149
  }
119
150
 
120
151
  // Create a real channel using the client
121
- const channel = client.channel('messaging', 'storybook-channel-1', channelData)
152
+ const channel = client.channel(
153
+ 'messaging',
154
+ 'storybook-channel-1',
155
+ channelData
156
+ )
122
157
 
123
158
  // Mock the watch method to return mocked data
124
159
  channel.watch = async () => {
@@ -142,6 +177,7 @@ const createMockChannel = async (
142
177
  user_id: participant.id,
143
178
  role: 'member',
144
179
  is_account: true,
180
+ customerTag: participantCustomerTag,
145
181
  },
146
182
  }
147
183
  return {
@@ -176,12 +212,16 @@ type TemplateProps = ComponentProps & {
176
212
  name?: string
177
213
  image?: string
178
214
  }
215
+ participantCustomerTag?: string
216
+ hasScrollableMessages?: boolean
179
217
  }
180
218
 
181
219
  const Template: StoryFn<TemplateProps> = (args) => {
182
220
  const {
183
221
  followerStatus,
184
222
  isFrozen = false,
223
+ participantCustomerTag,
224
+ hasScrollableMessages,
185
225
  typingUser,
186
226
  ...channelViewProps
187
227
  } = args
@@ -193,16 +233,27 @@ const Template: StoryFn<TemplateProps> = (args) => {
193
233
  client.user = mockUser
194
234
  return client
195
235
  })
196
-
236
+
197
237
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
198
238
 
199
239
  useEffect(() => {
200
- createMockChannel(client, true, followerStatus, isFrozen).then(
201
- (mockChannel) => {
202
- setChannel(mockChannel)
203
- }
204
- )
205
- }, [client, followerStatus, isFrozen])
240
+ createMockChannel(
241
+ client,
242
+ true,
243
+ followerStatus,
244
+ isFrozen,
245
+ participantCustomerTag,
246
+ hasScrollableMessages
247
+ ).then((mockChannel) => {
248
+ setChannel(mockChannel)
249
+ })
250
+ }, [
251
+ client,
252
+ followerStatus,
253
+ isFrozen,
254
+ participantCustomerTag,
255
+ hasScrollableMessages,
256
+ ])
206
257
 
207
258
  useEffect(() => {
208
259
  if (!channel || !typingUser) {
@@ -256,6 +307,9 @@ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
256
307
  )
257
308
  ParticipantIdentityTrigger.args = {
258
309
  showBackButton: false,
310
+ showStarButton: true,
311
+ participantCustomerTag: 'CUSTOMER_PAID',
312
+ hasScrollableMessages: true,
259
313
  onParticipantNameClick: () =>
260
314
  // eslint-disable-next-line no-alert
261
315
  window.alert('Participant details clicked — open Thread content'),
@@ -264,7 +318,7 @@ ParticipantIdentityTrigger.parameters = {
264
318
  docs: {
265
319
  description: {
266
320
  story:
267
- 'Header with `onParticipantNameClick` set: the participant avatar and name render as one button with a trailing caret in both the mobile and desktop header layouts. Click the identity block to fire the callback (shows an alert here). With the prop omitted (see Default) the header remains non-interactive.',
321
+ 'Header with `onParticipantNameClick` set: the redesigned floating header centers the avatar and name pill, shows paid/star badges when present, and fires the callback when clicked (shows an alert here). With the prop omitted (see Default) the previous header remains in use.',
268
322
  },
269
323
  },
270
324
  }
@@ -542,7 +596,7 @@ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
542
596
  client.user = mockUser
543
597
  return client
544
598
  })
545
-
599
+
546
600
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
547
601
 
548
602
  useEffect(() => {
@@ -22,7 +22,9 @@ vi.mock('stream-chat-react', () => ({
22
22
  Window: ({ children }: { children: React.ReactNode }) => (
23
23
  <div data-testid="window">{children}</div>
24
24
  ),
25
- MessageList: () => <div data-testid="message-list" />,
25
+ MessageList: ({ head }: { head?: React.ReactElement }) => (
26
+ <div data-testid="message-list">{head}</div>
27
+ ),
26
28
  WithComponents: ({ children }: { children: React.ReactNode }) => (
27
29
  <>{children}</>
28
30
  ),
@@ -100,9 +102,11 @@ const createChannel = ({
100
102
  username: undefined as string | undefined,
101
103
  is_account: true,
102
104
  },
105
+ otherMemberCustomerTag,
103
106
  }: {
104
107
  currentUserId?: 'visitor-1' | 'linker-1'
105
108
  currentUserRole?: 'owner' | 'member'
109
+ otherMemberCustomerTag?: string
106
110
  otherUser?: {
107
111
  id: string
108
112
  name?: string
@@ -134,6 +138,7 @@ const createChannel = ({
134
138
  is_account:
135
139
  otherUser.is_account ?? currentUserId === 'visitor-1',
136
140
  },
141
+ customerTag: otherMemberCustomerTag,
137
142
  role: currentUserRole === 'owner' ? 'member' : 'owner',
138
143
  },
139
144
  },
@@ -203,7 +208,7 @@ describe('ChannelView', () => {
203
208
  )
204
209
  })
205
210
 
206
- it('renders the participant name as a button that fires onParticipantNameClick', () => {
211
+ it('renders the redesigned header button when onParticipantNameClick is provided', () => {
207
212
  const onParticipantNameClick = vi.fn()
208
213
  renderWithProviders(
209
214
  <ChannelView
@@ -212,29 +217,42 @@ describe('ChannelView', () => {
212
217
  />
213
218
  )
214
219
 
215
- // Both the mobile and desktop header layouts render the button (one is
216
- // hidden via CSS), so there is at least one match.
217
- const buttons = screen.getAllByRole('button', {
220
+ const button = screen.getByRole('button', {
218
221
  name: /view details for linker/i,
219
222
  })
220
- expect(buttons.length).toBeGreaterThanOrEqual(1)
221
- expect(buttons[0]).toHaveClass(
223
+ expect(button).toHaveClass(
222
224
  'appearance-none',
223
225
  'border-0',
224
226
  'bg-transparent',
225
- 'rounded-[12px]',
226
- 'px-1',
227
- 'py-0.5'
227
+ 'flex-col'
228
228
  )
229
229
 
230
230
  const avatars = screen.getAllByTestId('avatar')
231
231
  expect(avatars.length).toBeGreaterThanOrEqual(1)
232
- expect(avatars[0].closest('button')).toBe(buttons[0])
232
+ expect(avatars[0].closest('button')).toBe(button)
233
+ expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
233
234
 
234
- buttons[0].click()
235
+ button.click()
235
236
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
236
237
  })
237
238
 
239
+ it('renders redesigned header badges for paid and starred conversations', () => {
240
+ mockIsStarred = true
241
+
242
+ renderWithProviders(
243
+ <ChannelView
244
+ channel={createChannel({
245
+ otherMemberCustomerTag: 'CUSTOMER_PAID',
246
+ })}
247
+ onParticipantNameClick={vi.fn()}
248
+ showStarButton={true}
249
+ />
250
+ )
251
+
252
+ expect(screen.getByLabelText('Paid customer')).toBeInTheDocument()
253
+ expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
254
+ })
255
+
238
256
  it('renders the actions menu by default', () => {
239
257
  renderWithProviders(<ChannelView channel={createChannel()} />)
240
258
 
@@ -25,6 +25,7 @@ import { resolveParticipantDisplayName } from '../utils/resolveParticipantDispla
25
25
 
26
26
  import { Avatar } from './Avatar'
27
27
  import { ChannelActionsMenu } from './ChannelActionsMenu'
28
+ import { ChannelHeaderRedesign } from './ChannelHeaderRedesign'
28
29
  import { CustomDateSeparator } from './CustomDateSeparator'
29
30
  import { CustomMessage } from './CustomMessage'
30
31
  import { CustomMessageActions } from './CustomMessage/CustomMessageActions'
@@ -497,27 +498,28 @@ const ChannelViewInner: React.FC<{
497
498
  >
498
499
  <Window>
499
500
  {/* Custom Channel Header */}
500
- <div key="lt-channel-header">
501
- <CustomChannelHeader
502
- onBack={onBack}
503
- showBackButton={showBackButton}
504
- showChannelInfo={showChannelInfo}
505
- showStarButton={showStarButton}
506
- dmAgentEnabled={showDmAgentHeader}
507
- onLeaveConversation={onLeaveConversation}
508
- onBlockParticipant={onBlockParticipant}
509
- showDeleteConversation={showDeleteConversation}
510
- showBlockParticipant={showBlockParticipant}
511
- showReportParticipant={showReportParticipant}
512
- onDeleteConversationClick={onDeleteConversationClick}
513
- onBlockParticipantClick={onBlockParticipantClick}
514
- onReportParticipantClick={onReportParticipantClick}
515
- customChannelActions={customChannelActions}
516
- renderChannelActions={renderChannelActions}
517
- onParticipantNameClick={onParticipantNameClick}
518
- renderHeaderTitleBadges={renderHeaderTitleBadges}
519
- />
520
- </div>
501
+ {!onParticipantNameClick && (
502
+ <div key="lt-channel-header">
503
+ <CustomChannelHeader
504
+ onBack={onBack}
505
+ showBackButton={showBackButton}
506
+ showChannelInfo={showChannelInfo}
507
+ showStarButton={showStarButton}
508
+ dmAgentEnabled={showDmAgentHeader}
509
+ onLeaveConversation={onLeaveConversation}
510
+ onBlockParticipant={onBlockParticipant}
511
+ showDeleteConversation={showDeleteConversation}
512
+ showBlockParticipant={showBlockParticipant}
513
+ showReportParticipant={showReportParticipant}
514
+ onDeleteConversationClick={onDeleteConversationClick}
515
+ onBlockParticipantClick={onBlockParticipantClick}
516
+ onReportParticipantClick={onReportParticipantClick}
517
+ customChannelActions={customChannelActions}
518
+ renderChannelActions={renderChannelActions}
519
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
520
+ />
521
+ </div>
522
+ )}
521
523
 
522
524
  {/* Custom Banner/Summary */}
523
525
  {renderChannelBanner ? (
@@ -531,7 +533,33 @@ const ChannelViewInner: React.FC<{
531
533
  key="lt-channel-message-list"
532
534
  className="flex-1 overflow-hidden relative"
533
535
  >
534
- <MessageList hideDeletedMessages hideNewMessageSeparator={false} />
536
+ <MessageList
537
+ head={
538
+ onParticipantNameClick ? (
539
+ <ChannelHeaderRedesign
540
+ onBack={onBack}
541
+ showBackButton={showBackButton}
542
+ showChannelInfo={showChannelInfo}
543
+ showStarButton={showStarButton}
544
+ dmAgentEnabled={showDmAgentHeader}
545
+ onLeaveConversation={onLeaveConversation}
546
+ onBlockParticipant={onBlockParticipant}
547
+ showDeleteConversation={showDeleteConversation}
548
+ showBlockParticipant={showBlockParticipant}
549
+ showReportParticipant={showReportParticipant}
550
+ onDeleteConversationClick={onDeleteConversationClick}
551
+ onBlockParticipantClick={onBlockParticipantClick}
552
+ onReportParticipantClick={onReportParticipantClick}
553
+ customChannelActions={customChannelActions}
554
+ renderChannelActions={renderChannelActions}
555
+ onParticipantNameClick={onParticipantNameClick}
556
+ renderHeaderTitleBadges={renderHeaderTitleBadges}
557
+ />
558
+ ) : undefined
559
+ }
560
+ hideDeletedMessages
561
+ hideNewMessageSeparator={false}
562
+ />
535
563
  </div>
536
564
 
537
565
  {renderConversationFooter ? (