@linktr.ee/messaging-react 3.4.1 → 3.4.2-rc-1782453401
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.
- package/dist/{Card-BKd0E-B9.js → Card-52LnCirC.js} +3 -3
- package/dist/{Card-BKd0E-B9.js.map → Card-52LnCirC.js.map} +1 -1
- package/dist/{Card-DaalkoQW.cjs → Card-BIa-47Ne.cjs} +2 -2
- package/dist/{Card-DaalkoQW.cjs.map → Card-BIa-47Ne.cjs.map} +1 -1
- package/dist/{Card-XVTz8IYz.js → Card-BaXLsSK2.js} +2 -2
- package/dist/{Card-XVTz8IYz.js.map → Card-BaXLsSK2.js.map} +1 -1
- package/dist/{Card-CdaZxDEx.cjs → Card-BkVj6kGN.cjs} +2 -2
- package/dist/{Card-CdaZxDEx.cjs.map → Card-BkVj6kGN.cjs.map} +1 -1
- package/dist/{Card-DXWp0uwi.cjs → Card-CcTyYY9N.cjs} +2 -2
- package/dist/{Card-DXWp0uwi.cjs.map → Card-CcTyYY9N.cjs.map} +1 -1
- package/dist/{Card-CrTBu2I6.js → Card-ql9pImRa.js} +2 -2
- package/dist/{Card-CrTBu2I6.js.map → Card-ql9pImRa.js.map} +1 -1
- package/dist/{LockedThumbnail-D_44b-Uy.js → LockedThumbnail-C3YOnS-o.js} +2 -2
- package/dist/{LockedThumbnail-D_44b-Uy.js.map → LockedThumbnail-C3YOnS-o.js.map} +1 -1
- package/dist/{LockedThumbnail-D3Xwdb5_.cjs → LockedThumbnail-D0ASm7LP.cjs} +2 -2
- package/dist/{LockedThumbnail-D3Xwdb5_.cjs.map → LockedThumbnail-D0ASm7LP.cjs.map} +1 -1
- package/dist/{index-DPGZRLRM.js → index-CbWEp3Zs.js} +1172 -1121
- package/dist/index-CbWEp3Zs.js.map +1 -0
- package/dist/index-DkqM2z_j.cjs +2 -0
- package/dist/index-DkqM2z_j.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +63 -0
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +56 -6
- package/src/components/ChannelList/CustomChannelPreview.tsx +7 -10
- package/src/components/ChannelView.stories.tsx +38 -7
- package/src/components/ChannelView.test.tsx +29 -15
- package/src/components/ChannelView.tsx +19 -4
- package/src/components/ParticipantBadges.tsx +39 -0
- package/src/hooks/useChannelStar.ts +9 -6
- package/src/stream-custom-data.ts +16 -1
- package/dist/index-Cds2w5zi.cjs +0 -2
- package/dist/index-Cds2w5zi.cjs.map +0 -1
- package/dist/index-DPGZRLRM.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-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-DkqM2z_j.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.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1471,6 +1471,13 @@ declare module 'stream-chat' {
|
|
|
1471
1471
|
*/
|
|
1472
1472
|
chatbot_paused?: boolean;
|
|
1473
1473
|
}
|
|
1474
|
+
interface CustomMemberData {
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether the member should be identified as a customer in participant UI.
|
|
1477
|
+
* Stored on the channel member so it is scoped to this conversation.
|
|
1478
|
+
*/
|
|
1479
|
+
customer?: boolean;
|
|
1480
|
+
}
|
|
1474
1481
|
interface CustomMessageData {
|
|
1475
1482
|
/**
|
|
1476
1483
|
* When true, hides the date timestamp in system messages.
|
|
@@ -1488,5 +1495,10 @@ declare module 'stream-chat' {
|
|
|
1488
1495
|
*/
|
|
1489
1496
|
dm_agent_system_type?: DmAgentSystemType;
|
|
1490
1497
|
}
|
|
1498
|
+
interface CustomUserData {
|
|
1499
|
+
is_account?: boolean;
|
|
1500
|
+
is_follower?: boolean;
|
|
1501
|
+
username?: string;
|
|
1502
|
+
}
|
|
1491
1503
|
}
|
|
1492
1504
|
|
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 g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-
|
|
1
|
+
import { a as e, b as t, C as i, c as n, d as o, e as m, F as g, f as l, L as r, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as v, o as A, p as C, q as F, s as k, t as b, u as f, v as x, w as y, x as P, y as S, z as q, B as z, D as B } from "./index-CbWEp3Zs.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.4.
|
|
3
|
+
"version": "3.4.2-rc-1782453401",
|
|
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": "
|
|
41
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1782453401",
|
|
42
42
|
"@phosphor-icons/react": "^2.1.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -55,6 +55,8 @@ const createMockChannel = (options: {
|
|
|
55
55
|
thumb_url?: string
|
|
56
56
|
}>
|
|
57
57
|
lastMessageMetadata?: CustomMessageData['metadata']
|
|
58
|
+
isCustomer?: boolean
|
|
59
|
+
isStarred?: boolean
|
|
58
60
|
}): Channel => {
|
|
59
61
|
const {
|
|
60
62
|
id,
|
|
@@ -66,6 +68,8 @@ const createMockChannel = (options: {
|
|
|
66
68
|
unreadCount = 0,
|
|
67
69
|
lastMessageAttachments,
|
|
68
70
|
lastMessageMetadata,
|
|
71
|
+
isCustomer = false,
|
|
72
|
+
isStarred = false,
|
|
69
73
|
} = options
|
|
70
74
|
|
|
71
75
|
const eventListeners = new Map<string, Set<(...args: unknown[]) => void>>()
|
|
@@ -89,8 +93,12 @@ const createMockChannel = (options: {
|
|
|
89
93
|
image: participantImage,
|
|
90
94
|
},
|
|
91
95
|
user_id: participantId,
|
|
96
|
+
...(isCustomer ? { customer: true } : {}),
|
|
92
97
|
},
|
|
93
98
|
},
|
|
99
|
+
membership: {
|
|
100
|
+
pinned_at: isStarred ? now().toISOString() : null,
|
|
101
|
+
},
|
|
94
102
|
messages:
|
|
95
103
|
lastMessageText || lastMessageAttachments || lastMessageMetadata
|
|
96
104
|
? ([
|
|
@@ -249,6 +257,61 @@ ChatbotMessage.args = {
|
|
|
249
257
|
}),
|
|
250
258
|
}
|
|
251
259
|
|
|
260
|
+
export const ParticipantBadges: StoryFn = () => {
|
|
261
|
+
const channels = [
|
|
262
|
+
createMockChannel({
|
|
263
|
+
id: 'channel-customer',
|
|
264
|
+
participantName: 'Alice Customer',
|
|
265
|
+
participantId: 'participant-customer',
|
|
266
|
+
participantImage: 'https://i.pravatar.cc/150?img=40',
|
|
267
|
+
lastMessageText: 'I bought this from your Linktree',
|
|
268
|
+
lastMessageTime: minutesAgo(2),
|
|
269
|
+
isCustomer: true,
|
|
270
|
+
}),
|
|
271
|
+
createMockChannel({
|
|
272
|
+
id: 'channel-starred',
|
|
273
|
+
participantName: 'Bob Starred',
|
|
274
|
+
participantId: 'participant-starred',
|
|
275
|
+
participantImage: 'https://i.pravatar.cc/150?img=41',
|
|
276
|
+
lastMessageText: 'Following up on this thread',
|
|
277
|
+
lastMessageTime: minutesAgo(8),
|
|
278
|
+
isStarred: true,
|
|
279
|
+
}),
|
|
280
|
+
createMockChannel({
|
|
281
|
+
id: 'channel-customer-starred',
|
|
282
|
+
participantName: 'Carol Customer',
|
|
283
|
+
participantId: 'participant-customer-starred',
|
|
284
|
+
participantImage: 'https://i.pravatar.cc/150?img=42',
|
|
285
|
+
lastMessageText: 'Thanks for the quick reply',
|
|
286
|
+
lastMessageTime: minutesAgo(15),
|
|
287
|
+
isCustomer: true,
|
|
288
|
+
isStarred: true,
|
|
289
|
+
}),
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="w-[360px] bg-chalk border border-sand rounded-lg overflow-hidden">
|
|
294
|
+
{channels.map((channel) => (
|
|
295
|
+
<CustomChannelPreview
|
|
296
|
+
key={channel.id}
|
|
297
|
+
channel={channel}
|
|
298
|
+
onChannelSelect={(channel) => {
|
|
299
|
+
console.log('Channel selected:', channel.id)
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
ParticipantBadges.parameters = {
|
|
307
|
+
docs: {
|
|
308
|
+
description: {
|
|
309
|
+
story:
|
|
310
|
+
'Shows the built-in customer badge from participant.member customer data and the starred badge from the current user membership pinned_at value.',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
|
|
252
315
|
export const TipMessage: StoryFn<ComponentProps> = Template.bind({})
|
|
253
316
|
TipMessage.args = {
|
|
254
317
|
channel: createMockChannel({
|
|
@@ -12,12 +12,15 @@ const mockUser = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
type MockChannel = Channel & {
|
|
15
|
-
emitMemberUpdated: (member?: {
|
|
15
|
+
emitMemberUpdated: (member?: {
|
|
16
|
+
pinned_at?: string | null
|
|
17
|
+
user_id?: string
|
|
18
|
+
}) => void
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
|
|
19
22
|
const memberUpdatedListeners = new Set<
|
|
20
|
-
(event: { member?: { pinned_at?: string | null } }) => void
|
|
23
|
+
(event: { member?: { pinned_at?: string | null; user_id?: string } }) => void
|
|
21
24
|
>()
|
|
22
25
|
|
|
23
26
|
return {
|
|
@@ -38,24 +41,34 @@ const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
|
|
|
38
41
|
on: vi.fn(
|
|
39
42
|
(
|
|
40
43
|
eventName: string,
|
|
41
|
-
listener: (event: {
|
|
44
|
+
listener: (event: {
|
|
45
|
+
member?: { pinned_at?: string | null; user_id?: string }
|
|
46
|
+
}) => void
|
|
42
47
|
) => {
|
|
43
48
|
if (eventName === 'member.updated') {
|
|
44
49
|
memberUpdatedListeners.add(listener)
|
|
45
50
|
}
|
|
51
|
+
return {
|
|
52
|
+
unsubscribe: () => memberUpdatedListeners.delete(listener),
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
),
|
|
48
56
|
off: vi.fn(
|
|
49
57
|
(
|
|
50
58
|
eventName: string,
|
|
51
|
-
listener: (event: {
|
|
59
|
+
listener: (event: {
|
|
60
|
+
member?: { pinned_at?: string | null; user_id?: string }
|
|
61
|
+
}) => void
|
|
52
62
|
) => {
|
|
53
63
|
if (eventName === 'member.updated') {
|
|
54
64
|
memberUpdatedListeners.delete(listener)
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
),
|
|
58
|
-
emitMemberUpdated: (member?: {
|
|
68
|
+
emitMemberUpdated: (member?: {
|
|
69
|
+
pinned_at?: string | null
|
|
70
|
+
user_id?: string
|
|
71
|
+
}) => {
|
|
59
72
|
memberUpdatedListeners.forEach((listener) => listener({ member }))
|
|
60
73
|
},
|
|
61
74
|
} as unknown as MockChannel
|
|
@@ -253,6 +266,21 @@ describe('CustomChannelPreview', () => {
|
|
|
253
266
|
expect(screen.getByText('No messages yet')).toBeInTheDocument()
|
|
254
267
|
})
|
|
255
268
|
|
|
269
|
+
it('shows a customer badge from participant member data', () => {
|
|
270
|
+
const channel = createMockChannel([])
|
|
271
|
+
channel.state.members['participant-1'] = {
|
|
272
|
+
user: { id: 'participant-1', name: 'Alice' },
|
|
273
|
+
user_id: 'participant-1',
|
|
274
|
+
customer: true,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
renderWithProviders(
|
|
278
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
expect(screen.getByRole('img', { name: 'Customer' })).toBeInTheDocument()
|
|
282
|
+
})
|
|
283
|
+
|
|
256
284
|
it('updates starred state when the channel membership changes', () => {
|
|
257
285
|
const channel = createMockChannel([])
|
|
258
286
|
|
|
@@ -265,11 +293,33 @@ describe('CustomChannelPreview', () => {
|
|
|
265
293
|
).not.toBeInTheDocument()
|
|
266
294
|
|
|
267
295
|
act(() => {
|
|
268
|
-
channel.emitMemberUpdated({
|
|
296
|
+
channel.emitMemberUpdated({
|
|
297
|
+
pinned_at: new Date().toISOString(),
|
|
298
|
+
user_id: mockUser.id,
|
|
299
|
+
})
|
|
269
300
|
})
|
|
270
301
|
|
|
271
302
|
expect(
|
|
272
303
|
screen.getByRole('heading', { name: /starred conversation/i })
|
|
273
304
|
).toBeInTheDocument()
|
|
274
305
|
})
|
|
306
|
+
|
|
307
|
+
it('ignores starred updates for other members', () => {
|
|
308
|
+
const channel = createMockChannel([])
|
|
309
|
+
|
|
310
|
+
renderWithProviders(
|
|
311
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
act(() => {
|
|
315
|
+
channel.emitMemberUpdated({
|
|
316
|
+
pinned_at: new Date().toISOString(),
|
|
317
|
+
user_id: 'participant-1',
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
expect(
|
|
322
|
+
screen.queryByRole('heading', { name: /starred conversation/i })
|
|
323
|
+
).not.toBeInTheDocument()
|
|
324
|
+
})
|
|
275
325
|
})
|
|
@@ -8,6 +8,7 @@ import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
|
|
|
8
8
|
import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
|
|
9
9
|
import { Avatar } from '../Avatar'
|
|
10
10
|
import { isChatbotMessage } from '../CustomMessage/MessageTag'
|
|
11
|
+
import { ParticipantBadges } from '../ParticipantBadges'
|
|
11
12
|
|
|
12
13
|
import { useChannelListContext } from './ChannelListContext'
|
|
13
14
|
|
|
@@ -139,22 +140,18 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
139
140
|
name={participantName}
|
|
140
141
|
image={participantImage}
|
|
141
142
|
size={48}
|
|
142
|
-
starred={isChannelStarred}
|
|
143
143
|
/>
|
|
144
144
|
|
|
145
145
|
{/* Content column */}
|
|
146
146
|
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
147
147
|
{/* Name and timestamp row */}
|
|
148
148
|
<div className="flex items-center justify-between gap-2">
|
|
149
|
-
<h3
|
|
150
|
-
className={
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<span className="sr-only">Starred conversation. </span>
|
|
156
|
-
)}
|
|
157
|
-
{participantName}
|
|
149
|
+
<h3 className="flex min-w-0 items-center gap-1 text-sm font-medium text-[#191918]">
|
|
150
|
+
<span className="min-w-0 truncate">{participantName}</span>
|
|
151
|
+
<ParticipantBadges
|
|
152
|
+
participant={participant}
|
|
153
|
+
isStarred={isChannelStarred}
|
|
154
|
+
/>
|
|
158
155
|
</h3>
|
|
159
156
|
{lastMessageTime && (
|
|
160
157
|
<span className="text-xs text-[#717070] flex-shrink-0">
|
|
@@ -41,7 +41,14 @@ const createMockChannel = async (
|
|
|
41
41
|
client: StreamChat,
|
|
42
42
|
hasMessages = true,
|
|
43
43
|
followerStatus?: string | boolean,
|
|
44
|
-
isFrozen = false
|
|
44
|
+
isFrozen = false,
|
|
45
|
+
{
|
|
46
|
+
isCustomer = false,
|
|
47
|
+
isStarred = false,
|
|
48
|
+
}: {
|
|
49
|
+
isCustomer?: boolean
|
|
50
|
+
isStarred?: boolean
|
|
51
|
+
} = {}
|
|
45
52
|
) => {
|
|
46
53
|
const participant = mockParticipants[0]
|
|
47
54
|
|
|
@@ -142,8 +149,12 @@ const createMockChannel = async (
|
|
|
142
149
|
user_id: participant.id,
|
|
143
150
|
role: 'member',
|
|
144
151
|
is_account: true,
|
|
152
|
+
...(isCustomer ? { customer: true } : {}),
|
|
145
153
|
},
|
|
146
154
|
}
|
|
155
|
+
channel.state.membership = {
|
|
156
|
+
pinned_at: isStarred ? now().toISOString() : null,
|
|
157
|
+
} as ChannelMemberResponse
|
|
147
158
|
return {
|
|
148
159
|
channel: channelData,
|
|
149
160
|
members: [],
|
|
@@ -171,6 +182,8 @@ const createMockChannel = async (
|
|
|
171
182
|
type TemplateProps = ComponentProps & {
|
|
172
183
|
followerStatus?: string | boolean
|
|
173
184
|
isFrozen?: boolean
|
|
185
|
+
isCustomer?: boolean
|
|
186
|
+
isStarred?: boolean
|
|
174
187
|
typingUser?: {
|
|
175
188
|
id: string
|
|
176
189
|
name?: string
|
|
@@ -182,6 +195,8 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
182
195
|
const {
|
|
183
196
|
followerStatus,
|
|
184
197
|
isFrozen = false,
|
|
198
|
+
isCustomer = false,
|
|
199
|
+
isStarred = false,
|
|
185
200
|
typingUser,
|
|
186
201
|
...channelViewProps
|
|
187
202
|
} = args
|
|
@@ -197,12 +212,13 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
197
212
|
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
198
213
|
|
|
199
214
|
useEffect(() => {
|
|
200
|
-
createMockChannel(client, true, followerStatus, isFrozen
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
createMockChannel(client, true, followerStatus, isFrozen, {
|
|
216
|
+
isCustomer,
|
|
217
|
+
isStarred,
|
|
218
|
+
}).then((mockChannel) => {
|
|
219
|
+
setChannel(mockChannel)
|
|
220
|
+
})
|
|
221
|
+
}, [client, followerStatus, isCustomer, isFrozen, isStarred])
|
|
206
222
|
|
|
207
223
|
useEffect(() => {
|
|
208
224
|
if (!channel || !typingUser) {
|
|
@@ -307,6 +323,21 @@ WithHeaderTitleBadges.parameters = {
|
|
|
307
323
|
},
|
|
308
324
|
}
|
|
309
325
|
|
|
326
|
+
export const ParticipantBadges: StoryFn<TemplateProps> = Template.bind({})
|
|
327
|
+
ParticipantBadges.args = {
|
|
328
|
+
showBackButton: false,
|
|
329
|
+
isCustomer: true,
|
|
330
|
+
isStarred: true,
|
|
331
|
+
}
|
|
332
|
+
ParticipantBadges.parameters = {
|
|
333
|
+
docs: {
|
|
334
|
+
description: {
|
|
335
|
+
story:
|
|
336
|
+
'Shows the built-in customer badge from participant.customer and the starred badge from the current user membership pinned_at value in the channel header.',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
|
|
310
341
|
export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
|
|
311
342
|
{}
|
|
312
343
|
)
|
|
@@ -98,9 +98,11 @@ const createChannel = ({
|
|
|
98
98
|
username: undefined as string | undefined,
|
|
99
99
|
is_account: true,
|
|
100
100
|
},
|
|
101
|
+
otherMemberCustomer = false,
|
|
101
102
|
}: {
|
|
102
103
|
currentUserId?: 'visitor-1' | 'linker-1'
|
|
103
104
|
currentUserRole?: 'owner' | 'member'
|
|
105
|
+
otherMemberCustomer?: boolean
|
|
104
106
|
otherUser?: {
|
|
105
107
|
id: string
|
|
106
108
|
name?: string
|
|
@@ -133,6 +135,7 @@ const createChannel = ({
|
|
|
133
135
|
otherUser.is_account ?? currentUserId === 'visitor-1',
|
|
134
136
|
},
|
|
135
137
|
role: currentUserRole === 'owner' ? 'member' : 'owner',
|
|
138
|
+
customer: otherMemberCustomer,
|
|
136
139
|
},
|
|
137
140
|
},
|
|
138
141
|
membership: {},
|
|
@@ -287,38 +290,49 @@ describe('ChannelView', () => {
|
|
|
287
290
|
expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
|
|
288
291
|
})
|
|
289
292
|
|
|
290
|
-
it('does not pass starred to avatar when
|
|
293
|
+
it('does not pass starred to avatar when channel is starred', () => {
|
|
291
294
|
mockIsStarred = true
|
|
292
295
|
const channel = createChannel()
|
|
293
296
|
|
|
294
297
|
renderWithProviders(
|
|
295
|
-
<ChannelView channel={channel} showStarButton={
|
|
298
|
+
<ChannelView channel={channel} showStarButton={true} />
|
|
296
299
|
)
|
|
297
300
|
|
|
298
301
|
expect(avatarRenderCalls.length).toBeGreaterThan(0)
|
|
299
|
-
expect(avatarRenderCalls.every((call) =>
|
|
302
|
+
expect(avatarRenderCalls.every((call) => !('starred' in call))).toBe(true)
|
|
300
303
|
})
|
|
301
304
|
|
|
302
|
-
it('
|
|
303
|
-
mockIsStarred = true
|
|
304
|
-
const channel = createChannel()
|
|
305
|
-
|
|
305
|
+
it('renders built-in customer badges from participant member data', () => {
|
|
306
306
|
renderWithProviders(
|
|
307
|
-
<ChannelView
|
|
307
|
+
<ChannelView
|
|
308
|
+
channel={createChannel({ otherMemberCustomer: true })}
|
|
309
|
+
renderHeaderTitleBadges={() => (
|
|
310
|
+
<span data-testid="custom-header-badge">VIP</span>
|
|
311
|
+
)}
|
|
312
|
+
/>
|
|
308
313
|
)
|
|
309
314
|
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
const customerBadges = screen.getAllByRole('img', { name: 'Customer' })
|
|
316
|
+
const customBadges = screen.getAllByTestId('custom-header-badge')
|
|
317
|
+
|
|
318
|
+
expect(customerBadges.length).toBeGreaterThan(0)
|
|
319
|
+
expect(customBadges.length).toBe(customerBadges.length)
|
|
320
|
+
customerBadges.forEach((badge, index) => {
|
|
321
|
+
expect(
|
|
322
|
+
badge.compareDocumentPosition(customBadges[index]) &
|
|
323
|
+
Node.DOCUMENT_POSITION_FOLLOWING
|
|
324
|
+
).toBeTruthy()
|
|
325
|
+
})
|
|
312
326
|
})
|
|
313
327
|
|
|
314
|
-
it('
|
|
328
|
+
it('renders built-in starred badges from channel star state', () => {
|
|
315
329
|
mockIsStarred = true
|
|
316
|
-
const channel = createChannel()
|
|
317
330
|
|
|
318
|
-
renderWithProviders(<ChannelView channel={
|
|
331
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
319
332
|
|
|
320
|
-
expect(
|
|
321
|
-
|
|
333
|
+
expect(
|
|
334
|
+
screen.getAllByRole('img', { name: 'Starred conversation' }).length
|
|
335
|
+
).toBeGreaterThan(0)
|
|
322
336
|
})
|
|
323
337
|
|
|
324
338
|
it('passes dmAgentEnabled to avatar when dmAgentEnabled is true for visitor view', () => {
|
|
@@ -32,6 +32,7 @@ import CustomTypingIndicator from './CustomTypingIndicator'
|
|
|
32
32
|
import { DmAgentEnabledContext } from './CustomTypingIndicator/DmAgentContext'
|
|
33
33
|
import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
34
34
|
import { LoadingState } from './MessagingShell/LoadingState'
|
|
35
|
+
import { ParticipantBadges } from './ParticipantBadges'
|
|
35
36
|
|
|
36
37
|
const ICON_BTN_CLASS =
|
|
37
38
|
'size-10 rounded-full hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
|
|
@@ -136,7 +137,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
136
137
|
id={participant?.user?.id || channel.id || 'unknown'}
|
|
137
138
|
name={participantName}
|
|
138
139
|
image={participantImage}
|
|
139
|
-
starred={showStarButton && isStarred}
|
|
140
140
|
dmAgentEnabled={dmAgentEnabled}
|
|
141
141
|
size={48}
|
|
142
142
|
/>
|
|
@@ -148,6 +148,10 @@ const CustomChannelHeader: React.FC<{
|
|
|
148
148
|
className="flex max-w-full items-center gap-0.5 rounded text-center text-xs font-medium text-black/90 transition-opacity hover:opacity-70"
|
|
149
149
|
>
|
|
150
150
|
<span className="min-w-0 truncate">{participantName}</span>
|
|
151
|
+
<ParticipantBadges
|
|
152
|
+
participant={participant}
|
|
153
|
+
isStarred={isStarred}
|
|
154
|
+
/>
|
|
151
155
|
{headerTitleBadges && (
|
|
152
156
|
<span className="shrink-0">{headerTitleBadges}</span>
|
|
153
157
|
)}
|
|
@@ -156,6 +160,10 @@ const CustomChannelHeader: React.FC<{
|
|
|
156
160
|
) : (
|
|
157
161
|
<p className="flex max-w-full items-center justify-center gap-1 text-center text-xs font-medium text-black/90">
|
|
158
162
|
<span className="min-w-0 truncate">{participantName}</span>
|
|
163
|
+
<ParticipantBadges
|
|
164
|
+
participant={participant}
|
|
165
|
+
isStarred={isStarred}
|
|
166
|
+
/>
|
|
159
167
|
{headerTitleBadges && (
|
|
160
168
|
<span className="shrink-0">{headerTitleBadges}</span>
|
|
161
169
|
)}
|
|
@@ -228,7 +236,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
228
236
|
id={participant?.user?.id || channel.id || 'unknown'}
|
|
229
237
|
name={participantName}
|
|
230
238
|
image={participantImage}
|
|
231
|
-
starred={showStarButton && isStarred}
|
|
232
239
|
dmAgentEnabled={dmAgentEnabled}
|
|
233
240
|
size={48}
|
|
234
241
|
/>
|
|
@@ -241,6 +248,10 @@ const CustomChannelHeader: React.FC<{
|
|
|
241
248
|
className="flex min-w-0 max-w-full items-center gap-1 rounded font-medium text-black/90 transition-opacity hover:opacity-70"
|
|
242
249
|
>
|
|
243
250
|
<span className="min-w-0 truncate">{participantName}</span>
|
|
251
|
+
<ParticipantBadges
|
|
252
|
+
participant={participant}
|
|
253
|
+
isStarred={isStarred}
|
|
254
|
+
/>
|
|
244
255
|
{headerTitleBadges && (
|
|
245
256
|
<span className="shrink-0">{headerTitleBadges}</span>
|
|
246
257
|
)}
|
|
@@ -249,6 +260,10 @@ const CustomChannelHeader: React.FC<{
|
|
|
249
260
|
) : (
|
|
250
261
|
<h1 className="flex min-w-0 items-center gap-1 font-medium text-black/90">
|
|
251
262
|
<span className="min-w-0 truncate">{participantName}</span>
|
|
263
|
+
<ParticipantBadges
|
|
264
|
+
participant={participant}
|
|
265
|
+
isStarred={isStarred}
|
|
266
|
+
/>
|
|
252
267
|
{headerTitleBadges && (
|
|
253
268
|
<span className="shrink-0">{headerTitleBadges}</span>
|
|
254
269
|
)}
|
|
@@ -388,10 +403,10 @@ const ChannelViewInner: React.FC<{
|
|
|
388
403
|
}, [channel._client?.userID, channel.state?.members])
|
|
389
404
|
|
|
390
405
|
const currentUserIsAccount =
|
|
391
|
-
|
|
406
|
+
currentMember?.user?.is_account ??
|
|
392
407
|
(currentMember as { is_account?: boolean } | undefined)?.is_account
|
|
393
408
|
const participantIsAccount =
|
|
394
|
-
|
|
409
|
+
participant?.user?.is_account ??
|
|
395
410
|
(participant as { is_account?: boolean } | undefined)?.is_account
|
|
396
411
|
|
|
397
412
|
const showDmAgentHeader =
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CurrencyDollarIcon, StarIcon } from '@phosphor-icons/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { ChannelMemberResponse } from 'stream-chat'
|
|
4
|
+
|
|
5
|
+
export const ParticipantBadges: React.FC<{
|
|
6
|
+
participant?: ChannelMemberResponse
|
|
7
|
+
isStarred?: boolean
|
|
8
|
+
}> = ({ participant, isStarred = false }) => {
|
|
9
|
+
const isCustomer = participant?.customer === true
|
|
10
|
+
|
|
11
|
+
if (!isCustomer && !isStarred) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<span className="inline-flex shrink-0 items-center gap-1">
|
|
15
|
+
{isCustomer && (
|
|
16
|
+
<span
|
|
17
|
+
role="img"
|
|
18
|
+
aria-label="Customer"
|
|
19
|
+
className="inline-flex size-4 items-center justify-center rounded-full bg-[#22C55E] text-white"
|
|
20
|
+
>
|
|
21
|
+
<CurrencyDollarIcon
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
className="size-3"
|
|
24
|
+
weight="bold"
|
|
25
|
+
/>
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
{isStarred && (
|
|
29
|
+
<span
|
|
30
|
+
role="img"
|
|
31
|
+
aria-label="Starred conversation"
|
|
32
|
+
className="inline-flex size-4 items-center justify-center rounded-full bg-[#F6C343] text-white"
|
|
33
|
+
>
|
|
34
|
+
<StarIcon aria-hidden="true" className="size-3" weight="fill" />
|
|
35
|
+
</span>
|
|
36
|
+
)}
|
|
37
|
+
</span>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
|
-
import { Channel, Event } from 'stream-chat'
|
|
2
|
+
import type { Channel, Event } from 'stream-chat'
|
|
3
3
|
|
|
4
4
|
export const useChannelStar = (channel?: Channel) => {
|
|
5
5
|
const [isChannelStarred, setIsChannelStarred] = useState(
|
|
@@ -15,15 +15,18 @@ export const useChannelStar = (channel?: Channel) => {
|
|
|
15
15
|
setIsChannelStarred(!!channel.state.membership?.pinned_at)
|
|
16
16
|
|
|
17
17
|
const handleMemberUpdate = (event: Event) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const currentUserId = channel._client?.userID
|
|
19
|
+
const memberUserId = event.member?.user_id ?? event.member?.user?.id
|
|
20
|
+
|
|
21
|
+
if (!currentUserId || memberUserId !== currentUserId) return
|
|
22
|
+
|
|
23
|
+
setIsChannelStarred(!!event.member?.pinned_at)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
channel.on('member.updated', handleMemberUpdate)
|
|
26
|
+
const subscription = channel.on('member.updated', handleMemberUpdate)
|
|
24
27
|
|
|
25
28
|
return () => {
|
|
26
|
-
|
|
29
|
+
subscription.unsubscribe()
|
|
27
30
|
}
|
|
28
31
|
}, [channel])
|
|
29
32
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Stream Chat custom data type augmentation.
|
|
3
3
|
*
|
|
4
4
|
* This file extends Stream Chat's type system by augmenting their empty
|
|
5
|
-
* CustomMessageData
|
|
5
|
+
* CustomMessageData, CustomChannelData, CustomMemberData, and CustomUserData
|
|
6
|
+
* interfaces with our custom properties.
|
|
6
7
|
*
|
|
7
8
|
* @see https://getstream.io/chat/docs/sdk/react/guides/typescript_and_custom_data_types/
|
|
8
9
|
*
|
|
@@ -98,6 +99,14 @@ declare module 'stream-chat' {
|
|
|
98
99
|
chatbot_paused?: boolean
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
interface CustomMemberData {
|
|
103
|
+
/**
|
|
104
|
+
* Whether the member should be identified as a customer in participant UI.
|
|
105
|
+
* Stored on the channel member so it is scoped to this conversation.
|
|
106
|
+
*/
|
|
107
|
+
customer?: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
interface CustomMessageData {
|
|
102
111
|
/**
|
|
103
112
|
* When true, hides the date timestamp in system messages.
|
|
@@ -115,4 +124,10 @@ declare module 'stream-chat' {
|
|
|
115
124
|
*/
|
|
116
125
|
dm_agent_system_type?: DmAgentSystemType
|
|
117
126
|
}
|
|
127
|
+
|
|
128
|
+
interface CustomUserData {
|
|
129
|
+
is_account?: boolean
|
|
130
|
+
is_follower?: boolean
|
|
131
|
+
username?: string
|
|
132
|
+
}
|
|
118
133
|
}
|