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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/{Card-d0_xEfdM.cjs → Card-6FOd4_j_.cjs} +2 -2
  2. package/dist/{Card-d0_xEfdM.cjs.map → Card-6FOd4_j_.cjs.map} +1 -1
  3. package/dist/{Card-DcUPqd79.cjs → Card-BhIG7T7c.cjs} +2 -2
  4. package/dist/{Card-DcUPqd79.cjs.map → Card-BhIG7T7c.cjs.map} +1 -1
  5. package/dist/{Card-BuR6mbUu.cjs → Card-CA63InbA.cjs} +2 -2
  6. package/dist/{Card-BuR6mbUu.cjs.map → Card-CA63InbA.cjs.map} +1 -1
  7. package/dist/{Card-B0lzpuYf.js → Card-C_TfROjB.js} +3 -3
  8. package/dist/{Card-B0lzpuYf.js.map → Card-C_TfROjB.js.map} +1 -1
  9. package/dist/{Card-BrC7mKtr.js → Card-DN8P8IeT.js} +2 -2
  10. package/dist/{Card-BrC7mKtr.js.map → Card-DN8P8IeT.js.map} +1 -1
  11. package/dist/{Card-CxS7GNqp.js → Card-DgWCIgWw.js} +2 -2
  12. package/dist/{Card-CxS7GNqp.js.map → Card-DgWCIgWw.js.map} +1 -1
  13. package/dist/{LockedThumbnail-BaDgF8qJ.js → LockedThumbnail-DNy4CoCm.js} +2 -2
  14. package/dist/{LockedThumbnail-BaDgF8qJ.js.map → LockedThumbnail-DNy4CoCm.js.map} +1 -1
  15. package/dist/{LockedThumbnail-BDtKMJjz.cjs → LockedThumbnail-DPlNLdpK.cjs} +2 -2
  16. package/dist/{LockedThumbnail-BDtKMJjz.cjs.map → LockedThumbnail-DPlNLdpK.cjs.map} +1 -1
  17. package/dist/index-ClREqfMm.cjs +2 -0
  18. package/dist/index-ClREqfMm.cjs.map +1 -0
  19. package/dist/{index-BPfehHDD.js → index-CxGOKFnx.js} +3786 -3876
  20. package/dist/index-CxGOKFnx.js.map +1 -0
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +1 -1
  23. package/package.json +2 -2
  24. package/src/components/ChannelHeaderRedesign.tsx +231 -0
  25. package/src/components/ChannelView.stories.tsx +68 -11
  26. package/src/components/ChannelView.test.tsx +54 -13
  27. package/src/components/ChannelView.tsx +74 -28
  28. package/src/components/CustomMessage/index.tsx +0 -67
  29. package/dist/index-BPfehHDD.js.map +0 -1
  30. package/dist/index-G4pnfm4m.cjs +0 -2
  31. package/dist/index-G4pnfm4m.cjs.map +0 -1
  32. package/src/components/CustomMessage/StreamAttachmentMessage.stories.tsx +0 -178
  33. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +0 -186
  34. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +0 -417
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-G4pnfm4m.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-ClREqfMm.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-BPfehHDD.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-CxGOKFnx.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.1",
3
+ "version": "3.6.2-rc-1783304704",
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-1783304704",
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
+ SparkleIcon,
6
+ StarIcon,
7
+ } from '@phosphor-icons/react'
8
+ import classNames from 'classnames'
9
+ import React from 'react'
10
+ import type { ChannelMemberResponse, Channel as ChannelType } from 'stream-chat'
11
+ import { useChannelStateContext, useChatContext } from 'stream-chat-react'
12
+
13
+ import { useChannelStar } from '../hooks/useChannelStar'
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
+ '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
+ const DM_AGENT_HEADER_HELPER_TEXT = 'Replies instantly with AI assistant'
25
+
26
+ const isCustomerPaidTag = (value: unknown) =>
27
+ typeof value === 'string' &&
28
+ value
29
+ .trim()
30
+ .replace(/[\s-]+/g, '_')
31
+ .toUpperCase() === CUSTOMER_PAID_TAG
32
+
33
+ const hasCustomerPaidTag = (participant?: ChannelMemberResponse) => {
34
+ const member = participant as
35
+ | {
36
+ customerTag?: unknown
37
+ customer_tag?: unknown
38
+ }
39
+ | undefined
40
+
41
+ return [member?.customerTag, member?.customer_tag].some(isCustomerPaidTag)
42
+ }
43
+
44
+ interface ChannelHeaderRedesignProps {
45
+ onBack?: () => void
46
+ showBackButton: boolean
47
+ showStarButton?: boolean
48
+ dmAgentEnabled?: boolean
49
+ onLeaveConversation?: (channel: ChannelType) => void
50
+ onBlockParticipant?: (participantId?: string) => void
51
+ showDeleteConversation?: boolean
52
+ showBlockParticipant?: boolean
53
+ showReportParticipant?: boolean
54
+ onDeleteConversationClick?: () => void
55
+ onBlockParticipantClick?: () => void
56
+ onReportParticipantClick?: () => void
57
+ customChannelActions?: React.ReactNode
58
+ renderChannelActions?: React.ReactNode
59
+ showChannelInfo?: boolean
60
+ onParticipantNameClick: () => void
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
+ }) => {
81
+ const { channel } = useChannelStateContext()
82
+ const { client } = useChatContext()
83
+
84
+ const participant = React.useMemo(
85
+ () =>
86
+ resolveConversationParticipant(
87
+ channel.state?.members,
88
+ client?.userID,
89
+ channel.type
90
+ ),
91
+ [channel.state?.members, client?.userID, channel.type]
92
+ )
93
+
94
+ const participantName = resolveParticipantDisplayName(participant?.user)
95
+ const participantImage = participant?.user?.image
96
+ const isStarred = useChannelStar(channel)
97
+ const isPaidCustomer = hasCustomerPaidTag(participant)
98
+
99
+ const handleStarClick = async () => {
100
+ try {
101
+ if (isStarred) {
102
+ await (channel as ChannelType).unpin()
103
+ } else {
104
+ await (channel as ChannelType).pin()
105
+ }
106
+ } catch (error) {
107
+ console.error(
108
+ '[ChannelHeaderRedesign] Failed to update pinned status:',
109
+ error
110
+ )
111
+ }
112
+ }
113
+
114
+ return (
115
+ <div className="@container w-full bg-[linear-gradient(180deg,#FBFAF9_0%,rgba(251,250,249,0.92)_58%,rgba(251,250,249,0)_100%)]">
116
+ <div className="grid min-h-[100px] grid-cols-[minmax(40px,1fr)_auto_minmax(40px,1fr)] items-start pb-2 pt-5 @lg:min-h-[104px] @lg:pt-6">
117
+ <div className="flex items-start">
118
+ {showBackButton && (
119
+ <button
120
+ className={classNames(
121
+ ICON_BTN_CLASS,
122
+ 'messaging-channel-view-back-button-mobile messaging-channel-view-back-button-desktop'
123
+ )}
124
+ onClick={onBack || (() => {})}
125
+ type="button"
126
+ aria-label="Back to conversations"
127
+ >
128
+ <ArrowLeftIcon className="size-5 text-black/90" />
129
+ </button>
130
+ )}
131
+ </div>
132
+
133
+ <div className="flex min-w-0 justify-center" data-dd-privacy="mask">
134
+ <button
135
+ type="button"
136
+ onClick={onParticipantNameClick}
137
+ aria-label={`View details for ${participantName}`}
138
+ className="group 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"
139
+ >
140
+ <Avatar
141
+ id={participant?.user?.id || channel.id || 'unknown'}
142
+ name={participantName}
143
+ image={participantImage}
144
+ dmAgentEnabled={dmAgentEnabled}
145
+ shape="circle"
146
+ size={48}
147
+ className="-mb-2"
148
+ />
149
+ <span className="relative z-10 flex max-w-full flex-col 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-filter duration-150 group-hover:brightness-95">
150
+ <span className="flex max-w-full items-center justify-center">
151
+ <span className="min-w-0 truncate text-sm font-bold leading-[1.43] tracking-[0.28px] text-black/90">
152
+ {participantName}
153
+ </span>
154
+ <span className="ml-2 flex gap-1">
155
+ {isPaidCustomer && (
156
+ <span
157
+ aria-label="Paid customer"
158
+ className="flex size-4 shrink-0 items-center justify-center rounded-full bg-[#34C759] text-white"
159
+ >
160
+ <CurrencyDollarIcon className="size-2.5" weight="bold" />
161
+ </span>
162
+ )}
163
+ {showStarButton && isStarred && (
164
+ <span
165
+ aria-label="Starred conversation"
166
+ className="flex size-4 shrink-0 items-center justify-center rounded-full bg-[#FFD60A] text-white"
167
+ >
168
+ <StarIcon className="size-2.5" weight="fill" />
169
+ </span>
170
+ )}
171
+ <CaretRightIcon
172
+ aria-hidden="true"
173
+ className="size-4 shrink-0 text-black/65"
174
+ weight="bold"
175
+ />
176
+ </span>
177
+ </span>
178
+ {dmAgentEnabled && (
179
+ <span className="mt-1 flex items-center gap-1 px-1 pb-0.5 text-[10px] leading-3 text-black/55">
180
+ <SparkleIcon className="size-3 shrink-0 text-black/55" />
181
+ <span>{DM_AGENT_HEADER_HELPER_TEXT}</span>
182
+ </span>
183
+ )}
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 {
@@ -169,6 +205,7 @@ const createMockChannel = async (
169
205
  }
170
206
 
171
207
  type TemplateProps = ComponentProps & {
208
+ hasMessages?: boolean
172
209
  followerStatus?: string | boolean
173
210
  isFrozen?: boolean
174
211
  typingUser?: {
@@ -176,12 +213,17 @@ type TemplateProps = ComponentProps & {
176
213
  name?: string
177
214
  image?: string
178
215
  }
216
+ participantCustomerTag?: string
217
+ hasScrollableMessages?: boolean
179
218
  }
180
219
 
181
220
  const Template: StoryFn<TemplateProps> = (args) => {
182
221
  const {
222
+ hasMessages = true,
183
223
  followerStatus,
184
224
  isFrozen = false,
225
+ participantCustomerTag,
226
+ hasScrollableMessages,
185
227
  typingUser,
186
228
  ...channelViewProps
187
229
  } = args
@@ -193,16 +235,28 @@ const Template: StoryFn<TemplateProps> = (args) => {
193
235
  client.user = mockUser
194
236
  return client
195
237
  })
196
-
238
+
197
239
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
198
240
 
199
241
  useEffect(() => {
200
- createMockChannel(client, true, followerStatus, isFrozen).then(
201
- (mockChannel) => {
202
- setChannel(mockChannel)
203
- }
204
- )
205
- }, [client, followerStatus, isFrozen])
242
+ createMockChannel(
243
+ client,
244
+ hasMessages,
245
+ followerStatus,
246
+ isFrozen,
247
+ participantCustomerTag,
248
+ hasScrollableMessages
249
+ ).then((mockChannel) => {
250
+ setChannel(mockChannel)
251
+ })
252
+ }, [
253
+ client,
254
+ hasMessages,
255
+ followerStatus,
256
+ isFrozen,
257
+ participantCustomerTag,
258
+ hasScrollableMessages,
259
+ ])
206
260
 
207
261
  useEffect(() => {
208
262
  if (!channel || !typingUser) {
@@ -256,6 +310,9 @@ export const ParticipantIdentityTrigger: StoryFn<TemplateProps> = Template.bind(
256
310
  )
257
311
  ParticipantIdentityTrigger.args = {
258
312
  showBackButton: false,
313
+ showStarButton: true,
314
+ participantCustomerTag: 'CUSTOMER_PAID',
315
+ hasScrollableMessages: true,
259
316
  onParticipantNameClick: () =>
260
317
  // eslint-disable-next-line no-alert
261
318
  window.alert('Participant details clicked — open Thread content'),
@@ -264,7 +321,7 @@ ParticipantIdentityTrigger.parameters = {
264
321
  docs: {
265
322
  description: {
266
323
  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.',
324
+ '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
325
  },
269
326
  },
270
327
  }
@@ -542,7 +599,7 @@ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
542
599
  client.user = mockUser
543
600
  return client
544
601
  })
545
-
602
+
546
603
  const [channel, setChannel] = React.useState<ChannelType | null>(null)
547
604
 
548
605
  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
  ),
@@ -94,15 +96,19 @@ vi.mock('../hooks/useChannelStar', () => ({
94
96
  const createChannel = ({
95
97
  currentUserId = 'visitor-1',
96
98
  currentUserRole = 'owner',
99
+ messages = [],
97
100
  otherUser = {
98
101
  id: 'linker-1' as const,
99
102
  name: 'Linker' as string | undefined,
100
103
  username: undefined as string | undefined,
101
104
  is_account: true,
102
105
  },
106
+ otherMemberCustomerTag,
103
107
  }: {
104
108
  currentUserId?: 'visitor-1' | 'linker-1'
105
109
  currentUserRole?: 'owner' | 'member'
110
+ messages?: Channel['state']['messages']
111
+ otherMemberCustomerTag?: string
106
112
  otherUser?: {
107
113
  id: string
108
114
  name?: string
@@ -134,11 +140,12 @@ const createChannel = ({
134
140
  is_account:
135
141
  otherUser.is_account ?? currentUserId === 'visitor-1',
136
142
  },
143
+ customerTag: otherMemberCustomerTag,
137
144
  role: currentUserRole === 'owner' ? 'member' : 'owner',
138
145
  },
139
146
  },
140
147
  membership: {},
141
- messages: [],
148
+ messages,
142
149
  },
143
150
  on: vi.fn(),
144
151
  off: vi.fn(),
@@ -203,7 +210,7 @@ describe('ChannelView', () => {
203
210
  )
204
211
  })
205
212
 
206
- it('renders the participant name as a button that fires onParticipantNameClick', () => {
213
+ it('renders the redesigned header button when onParticipantNameClick is provided', () => {
207
214
  const onParticipantNameClick = vi.fn()
208
215
  renderWithProviders(
209
216
  <ChannelView
@@ -212,29 +219,63 @@ describe('ChannelView', () => {
212
219
  />
213
220
  )
214
221
 
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', {
222
+ const button = screen.getByRole('button', {
218
223
  name: /view details for linker/i,
219
224
  })
220
- expect(buttons.length).toBeGreaterThanOrEqual(1)
221
- expect(buttons[0]).toHaveClass(
225
+ expect(button).toHaveClass(
222
226
  'appearance-none',
223
227
  'border-0',
224
228
  'bg-transparent',
225
- 'rounded-[12px]',
226
- 'px-1',
227
- 'py-0.5'
229
+ 'flex-col'
228
230
  )
229
231
 
230
232
  const avatars = screen.getAllByTestId('avatar')
231
233
  expect(avatars.length).toBeGreaterThanOrEqual(1)
232
- expect(avatars[0].closest('button')).toBe(buttons[0])
234
+ expect(avatars[0].closest('button')).toBe(button)
235
+ expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
233
236
 
234
- buttons[0].click()
237
+ button.click()
235
238
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
236
239
  })
237
240
 
241
+ it('renders redesigned header badges for paid and starred conversations', () => {
242
+ mockIsStarred = true
243
+
244
+ renderWithProviders(
245
+ <ChannelView
246
+ channel={createChannel({
247
+ otherMemberCustomerTag: 'CUSTOMER_PAID',
248
+ })}
249
+ onParticipantNameClick={vi.fn()}
250
+ showStarButton={true}
251
+ />
252
+ )
253
+
254
+ expect(screen.getByLabelText('Paid customer')).toBeInTheDocument()
255
+ expect(screen.getByLabelText('Starred conversation')).toBeInTheDocument()
256
+ })
257
+
258
+ it('renders the redesigned header outside MessageList head when all messages are deleted', () => {
259
+ renderWithProviders(
260
+ <ChannelView
261
+ channel={createChannel({
262
+ messages: [
263
+ {
264
+ id: 'deleted-message-1',
265
+ type: 'deleted',
266
+ } as Channel['state']['messages'][number],
267
+ ],
268
+ })}
269
+ onParticipantNameClick={vi.fn()}
270
+ />
271
+ )
272
+
273
+ const button = screen.getByRole('button', {
274
+ name: /view details for linker/i,
275
+ })
276
+ expect(button.closest('[data-testid="message-list"]')).toBeNull()
277
+ })
278
+
238
279
  it('renders the actions menu by default', () => {
239
280
  renderWithProviders(<ChannelView channel={createChannel()} />)
240
281