@linktr.ee/messaging-react 3.5.0-rc-1782480113 → 3.5.0-rc-1782792813
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-Dy1Qj3MR.js → Card-BWWBaNKF.js} +2 -2
- package/dist/{Card-Dy1Qj3MR.js.map → Card-BWWBaNKF.js.map} +1 -1
- package/dist/{Card-DhAhEetQ.cjs → Card-Bj4YaHav.cjs} +2 -2
- package/dist/{Card-DhAhEetQ.cjs.map → Card-Bj4YaHav.cjs.map} +1 -1
- package/dist/{Card-D34G7gvm.js → Card-BnGeMt4f.js} +2 -2
- package/dist/{Card-D34G7gvm.js.map → Card-BnGeMt4f.js.map} +1 -1
- package/dist/{Card-DVDoFfkP.js → Card-DkXzpFLA.js} +3 -3
- package/dist/{Card-DVDoFfkP.js.map → Card-DkXzpFLA.js.map} +1 -1
- package/dist/{Card-DIFgYVKx.cjs → Card-DpCwDviq.cjs} +2 -2
- package/dist/{Card-DIFgYVKx.cjs.map → Card-DpCwDviq.cjs.map} +1 -1
- package/dist/{Card-yHlBBfOt.cjs → Card-qrDaFHtJ.cjs} +2 -2
- package/dist/{Card-yHlBBfOt.cjs.map → Card-qrDaFHtJ.cjs.map} +1 -1
- package/dist/{LockedThumbnail-1S_Rjelj.js → LockedThumbnail-DJR--PAi.js} +2 -2
- package/dist/{LockedThumbnail-1S_Rjelj.js.map → LockedThumbnail-DJR--PAi.js.map} +1 -1
- package/dist/{LockedThumbnail-P2BVsLli.cjs → LockedThumbnail-DhXEk-NX.cjs} +2 -2
- package/dist/{LockedThumbnail-P2BVsLli.cjs.map → LockedThumbnail-DhXEk-NX.cjs.map} +1 -1
- package/dist/index-DzOA7Yu8.cjs +2 -0
- package/dist/index-DzOA7Yu8.cjs.map +1 -0
- package/dist/{index-CgczmQkU.js → index-ubCRSgQk.js} +1161 -1074
- package/dist/index-ubCRSgQk.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +15 -15
- package/dist/index.js +17 -18
- package/package.json +2 -2
- package/src/components/ChannelList/ChannelList.stories.tsx +5 -3
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +111 -1
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +58 -46
- package/src/components/ChannelList/CustomChannelPreview.tsx +12 -13
- package/src/components/ChannelList/index.test.tsx +25 -2
- package/src/components/ChannelList/index.tsx +2 -1
- package/src/components/ChannelView.stories.tsx +45 -12
- package/src/components/ChannelView.test.tsx +64 -62
- package/src/components/ChannelView.tsx +118 -68
- package/src/components/CustomMessage/index.tsx +1 -0
- package/src/components/CustomMessageInput/index.tsx +4 -1
- package/src/components/MediaMessage/index.tsx +13 -5
- package/src/components/ParticipantBadges.tsx +42 -0
- package/src/hooks/useChannelStar.ts +9 -6
- package/src/index.ts +0 -2
- package/src/stories/mocks.tsx +30 -5
- package/src/stream-custom-data.ts +12 -5
- package/src/types.ts +9 -1
- package/dist/index-BZjaMDgI.cjs +0 -2
- package/dist/index-BZjaMDgI.cjs.map +0 -1
- package/dist/index-CgczmQkU.js.map +0 -1
- package/src/components/VerifiedBadge/index.tsx +0 -25
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-DzOA7Yu8.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.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessageVote=e.useMessageVote;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Attachment } from 'stream-chat';
|
|
|
2
2
|
import { Channel } from 'stream-chat';
|
|
3
3
|
import { ChannelFilters } from 'stream-chat';
|
|
4
4
|
import { ChannelMemberResponse } from 'stream-chat';
|
|
5
|
+
import { ChannelPreviewUIComponentProps } from 'stream-chat-react';
|
|
5
6
|
import { ChannelSort } from 'stream-chat';
|
|
6
7
|
import { ComponentType } from 'react';
|
|
7
8
|
import { default as default_2 } from 'react';
|
|
@@ -132,6 +133,11 @@ export declare interface ChannelListProps {
|
|
|
132
133
|
*/
|
|
133
134
|
sort?: ChannelSort;
|
|
134
135
|
customEmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
|
|
136
|
+
/**
|
|
137
|
+
* Custom component rendered for each channel row in the conversation list.
|
|
138
|
+
* Defaults to the package's built-in CustomChannelPreview.
|
|
139
|
+
*/
|
|
140
|
+
channelPreview?: ComponentType<ChannelPreviewUIComponentProps>;
|
|
135
141
|
renderMessagePreview?: (message: LocalMessage | undefined, defaultPreview?: string) => React.ReactNode;
|
|
136
142
|
/**
|
|
137
143
|
* Language code used to pick translated message text from Stream Chat i18n.
|
|
@@ -1423,17 +1429,6 @@ declare interface UseMessageVoteResult {
|
|
|
1423
1429
|
*/
|
|
1424
1430
|
export declare const useMessaging: () => MessagingContextValue;
|
|
1425
1431
|
|
|
1426
|
-
/**
|
|
1427
|
-
* Linktree verification badge shown next to the name of a verified profile.
|
|
1428
|
-
* Rendered when a participant's Stream user has `is_verified` set to true.
|
|
1429
|
-
*/
|
|
1430
|
-
export declare const VerifiedBadge: default_2.FC<VerifiedBadgeProps>;
|
|
1431
|
-
|
|
1432
|
-
export declare interface VerifiedBadgeProps {
|
|
1433
|
-
className?: string;
|
|
1434
|
-
size?: number;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
1432
|
declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
1438
1433
|
/** Single video — convenience for the most common case. */
|
|
1439
1434
|
src?: string;
|
|
@@ -1506,12 +1501,12 @@ declare module 'stream-chat' {
|
|
|
1506
1501
|
*/
|
|
1507
1502
|
chatbot_paused?: boolean;
|
|
1508
1503
|
}
|
|
1509
|
-
interface
|
|
1504
|
+
interface CustomMemberData {
|
|
1510
1505
|
/**
|
|
1511
|
-
* Whether the
|
|
1512
|
-
*
|
|
1506
|
+
* Whether the member should be identified as a customer in participant UI.
|
|
1507
|
+
* Stored on the channel member so it is scoped to this conversation.
|
|
1513
1508
|
*/
|
|
1514
|
-
|
|
1509
|
+
customer?: boolean;
|
|
1515
1510
|
}
|
|
1516
1511
|
interface CustomMessageData {
|
|
1517
1512
|
/**
|
|
@@ -1530,5 +1525,10 @@ declare module 'stream-chat' {
|
|
|
1530
1525
|
*/
|
|
1531
1526
|
dm_agent_system_type?: DmAgentSystemType;
|
|
1532
1527
|
}
|
|
1528
|
+
interface CustomUserData {
|
|
1529
|
+
is_account?: boolean;
|
|
1530
|
+
is_follower?: boolean;
|
|
1531
|
+
username?: string;
|
|
1532
|
+
}
|
|
1533
1533
|
}
|
|
1534
1534
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as e, b as t, C as i, c as o, d as n, e as m, F as g, f as r, L as l, h as M, M as u, i as L, j as c, k as h, l as d, m as p, n as C, o as v, p as A, q as k, s as F, 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, E as D } from "./index-ubCRSgQk.js";
|
|
2
2
|
export {
|
|
3
|
-
|
|
3
|
+
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
|
5
5
|
i as ChannelEmptyState,
|
|
6
6
|
o as ChannelList,
|
|
@@ -13,24 +13,23 @@ export {
|
|
|
13
13
|
u as MediaMessage,
|
|
14
14
|
L as MessageAttachment,
|
|
15
15
|
c as MessageVoteButtons,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
p as
|
|
19
|
-
C as
|
|
20
|
-
v as
|
|
21
|
-
A as
|
|
22
|
-
k as
|
|
23
|
-
F as
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
b as messageAttachmentGroupPositionFromStream,
|
|
16
|
+
h as MessagingProvider,
|
|
17
|
+
d as MessagingShell,
|
|
18
|
+
p as buildCompactMetaLabel,
|
|
19
|
+
C as formatFileSize,
|
|
20
|
+
v as formatRelativeTime,
|
|
21
|
+
A as getFileExtensionLabel,
|
|
22
|
+
k as getMessageDisplayText,
|
|
23
|
+
F as isLinkAttachment,
|
|
24
|
+
b as isUuidLike,
|
|
25
|
+
f as messageAttachmentGroupPositionFromStream,
|
|
27
26
|
x as normalizeLanguageCode,
|
|
28
27
|
y as resolveLinkAttachment,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
P as resolveMediaFromMessage,
|
|
29
|
+
S as resolveParticipantDisplayName,
|
|
30
|
+
q as useComposerLocked,
|
|
31
|
+
z as useCustomMessage,
|
|
32
|
+
B as useMessageVote,
|
|
34
33
|
D as useMessaging
|
|
35
34
|
};
|
|
36
35
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.5.0-rc-
|
|
3
|
+
"version": "3.5.0-rc-1782792813",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@linktr.ee/component-library": "11.8.6",
|
|
41
|
-
"@linktr.ee/messaging-core": "2.3.0-rc-
|
|
41
|
+
"@linktr.ee/messaging-core": "2.3.0-rc-1782792813",
|
|
42
42
|
"@phosphor-icons/react": "^2.1.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { MockChatProviderWithChannels } from '../../stories/mocks'
|
|
5
5
|
|
|
6
6
|
import { ChannelList } from '.'
|
|
7
7
|
|
|
@@ -21,9 +21,9 @@ const meta: Meta<ComponentProps> = {
|
|
|
21
21
|
},
|
|
22
22
|
decorators: [
|
|
23
23
|
(Story) => (
|
|
24
|
-
<
|
|
24
|
+
<MockChatProviderWithChannels>
|
|
25
25
|
<Story />
|
|
26
|
-
</
|
|
26
|
+
</MockChatProviderWithChannels>
|
|
27
27
|
),
|
|
28
28
|
],
|
|
29
29
|
}
|
|
@@ -39,11 +39,13 @@ const Template: StoryFn<ComponentProps> = (args) => {
|
|
|
39
39
|
|
|
40
40
|
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
41
41
|
Default.args = {
|
|
42
|
+
filters: { type: 'messaging' },
|
|
42
43
|
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export const WithSelection: StoryFn<ComponentProps> = Template.bind({})
|
|
46
47
|
WithSelection.args = {
|
|
48
|
+
filters: { type: 'messaging' },
|
|
47
49
|
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
LocalMessage,
|
|
7
7
|
StreamChat,
|
|
8
8
|
} from 'stream-chat'
|
|
9
|
+
import type { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
9
10
|
|
|
10
11
|
import {
|
|
11
12
|
daysAgo,
|
|
@@ -14,13 +15,16 @@ import {
|
|
|
14
15
|
now,
|
|
15
16
|
secondsAgo,
|
|
16
17
|
} from '../../stories/decorators/storyTime'
|
|
18
|
+
import { MockChatProviderWithChannels } from '../../stories/mocks'
|
|
17
19
|
|
|
18
20
|
import CustomChannelPreview from './CustomChannelPreview'
|
|
19
21
|
|
|
22
|
+
import { ChannelList } from '.'
|
|
23
|
+
|
|
20
24
|
type ComponentProps = React.ComponentProps<typeof CustomChannelPreview>
|
|
21
25
|
|
|
22
26
|
const meta: Meta<ComponentProps> = {
|
|
23
|
-
title: 'ChannelList/
|
|
27
|
+
title: 'ChannelList/ChannelPreview',
|
|
24
28
|
component: CustomChannelPreview,
|
|
25
29
|
parameters: {
|
|
26
30
|
layout: 'centered',
|
|
@@ -55,6 +59,8 @@ const createMockChannel = (options: {
|
|
|
55
59
|
thumb_url?: string
|
|
56
60
|
}>
|
|
57
61
|
lastMessageMetadata?: CustomMessageData['metadata']
|
|
62
|
+
isCustomer?: boolean
|
|
63
|
+
isStarred?: boolean
|
|
58
64
|
}): Channel => {
|
|
59
65
|
const {
|
|
60
66
|
id,
|
|
@@ -66,6 +72,8 @@ const createMockChannel = (options: {
|
|
|
66
72
|
unreadCount = 0,
|
|
67
73
|
lastMessageAttachments,
|
|
68
74
|
lastMessageMetadata,
|
|
75
|
+
isCustomer = false,
|
|
76
|
+
isStarred = false,
|
|
69
77
|
} = options
|
|
70
78
|
|
|
71
79
|
const eventListeners = new Map<string, Set<(...args: unknown[]) => void>>()
|
|
@@ -89,8 +97,12 @@ const createMockChannel = (options: {
|
|
|
89
97
|
image: participantImage,
|
|
90
98
|
},
|
|
91
99
|
user_id: participantId,
|
|
100
|
+
...(isCustomer ? { customer: true } : {}),
|
|
92
101
|
},
|
|
93
102
|
},
|
|
103
|
+
membership: {
|
|
104
|
+
pinned_at: isStarred ? now().toISOString() : null,
|
|
105
|
+
},
|
|
94
106
|
messages:
|
|
95
107
|
lastMessageText || lastMessageAttachments || lastMessageMetadata
|
|
96
108
|
? ([
|
|
@@ -249,6 +261,61 @@ ChatbotMessage.args = {
|
|
|
249
261
|
}),
|
|
250
262
|
}
|
|
251
263
|
|
|
264
|
+
export const ParticipantBadges: StoryFn = () => {
|
|
265
|
+
const channels = [
|
|
266
|
+
createMockChannel({
|
|
267
|
+
id: 'channel-customer',
|
|
268
|
+
participantName: 'Alice Customer',
|
|
269
|
+
participantId: 'participant-customer',
|
|
270
|
+
participantImage: 'https://i.pravatar.cc/150?img=40',
|
|
271
|
+
lastMessageText: 'I bought this from your Linktree',
|
|
272
|
+
lastMessageTime: minutesAgo(2),
|
|
273
|
+
isCustomer: true,
|
|
274
|
+
}),
|
|
275
|
+
createMockChannel({
|
|
276
|
+
id: 'channel-starred',
|
|
277
|
+
participantName: 'Bob Starred',
|
|
278
|
+
participantId: 'participant-starred',
|
|
279
|
+
participantImage: 'https://i.pravatar.cc/150?img=41',
|
|
280
|
+
lastMessageText: 'Following up on this thread',
|
|
281
|
+
lastMessageTime: minutesAgo(8),
|
|
282
|
+
isStarred: true,
|
|
283
|
+
}),
|
|
284
|
+
createMockChannel({
|
|
285
|
+
id: 'channel-customer-starred',
|
|
286
|
+
participantName: 'Carol Customer',
|
|
287
|
+
participantId: 'participant-customer-starred',
|
|
288
|
+
participantImage: 'https://i.pravatar.cc/150?img=42',
|
|
289
|
+
lastMessageText: 'Thanks for the quick reply',
|
|
290
|
+
lastMessageTime: minutesAgo(15),
|
|
291
|
+
isCustomer: true,
|
|
292
|
+
isStarred: true,
|
|
293
|
+
}),
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="w-[360px] bg-chalk border border-sand rounded-lg overflow-hidden">
|
|
298
|
+
{channels.map((channel) => (
|
|
299
|
+
<CustomChannelPreview
|
|
300
|
+
key={channel.id}
|
|
301
|
+
channel={channel}
|
|
302
|
+
onChannelSelect={(channel) => {
|
|
303
|
+
console.log('Channel selected:', channel.id)
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
ParticipantBadges.parameters = {
|
|
311
|
+
docs: {
|
|
312
|
+
description: {
|
|
313
|
+
story:
|
|
314
|
+
'Shows the built-in customer badge from participant.member customer data and the starred badge from the current user membership pinned_at value.',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
|
|
252
319
|
export const TipMessage: StoryFn<ComponentProps> = Template.bind({})
|
|
253
320
|
TipMessage.args = {
|
|
254
321
|
channel: createMockChannel({
|
|
@@ -799,3 +866,46 @@ WithLongAttachmentUrl.args = {
|
|
|
799
866
|
}),
|
|
800
867
|
}
|
|
801
868
|
WithLongAttachmentUrl.parameters = skipInChromatic
|
|
869
|
+
|
|
870
|
+
const HostChannelPreview: React.FC<ChannelPreviewUIComponentProps> = ({
|
|
871
|
+
channel,
|
|
872
|
+
unread,
|
|
873
|
+
}) => {
|
|
874
|
+
const participant = Object.values(channel?.state?.members || {}).find(
|
|
875
|
+
(member) => member.user?.id !== channel?._client?.userID
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
return (
|
|
879
|
+
<div className="mx-3 mt-3 rounded-[20px] border border-[#e0dfdb] bg-white p-4 shadow-[0_4px_16px_rgba(0,0,0,0.06)]">
|
|
880
|
+
<div className="flex items-center justify-between">
|
|
881
|
+
<div className="min-w-0">
|
|
882
|
+
<p className="truncate text-sm font-semibold text-[#191918]">
|
|
883
|
+
{participant?.user?.name ?? 'Unknown visitor'}
|
|
884
|
+
</p>
|
|
885
|
+
<p className="mt-1 line-clamp-1 text-xs text-[#717070]">
|
|
886
|
+
Host-owned channel preview
|
|
887
|
+
</p>
|
|
888
|
+
</div>
|
|
889
|
+
{Boolean(unread) && (
|
|
890
|
+
<span className="ml-3 rounded-full bg-[#7f22fe] px-2 py-1 text-xs font-medium text-white">
|
|
891
|
+
{unread}
|
|
892
|
+
</span>
|
|
893
|
+
)}
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export const Custom: StoryFn = () => (
|
|
900
|
+
<MockChatProviderWithChannels>
|
|
901
|
+
<div className="h-screen w-full md:w-[360px] bg-chalk">
|
|
902
|
+
<ChannelList
|
|
903
|
+
filters={{ type: 'messaging' }}
|
|
904
|
+
onChannelSelect={(channel) => {
|
|
905
|
+
console.log('Channel selected:', channel.id)
|
|
906
|
+
}}
|
|
907
|
+
channelPreview={HostChannelPreview}
|
|
908
|
+
/>
|
|
909
|
+
</div>
|
|
910
|
+
</MockChatProviderWithChannels>
|
|
911
|
+
)
|
|
@@ -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
|
|
@@ -101,44 +114,6 @@ describe('CustomChannelPreview', () => {
|
|
|
101
114
|
expect(screen.getByText('Unknown member')).toBeInTheDocument()
|
|
102
115
|
})
|
|
103
116
|
|
|
104
|
-
it('shows the verification badge when the participant is verified', () => {
|
|
105
|
-
const channel = createMockChannel([])
|
|
106
|
-
channel.state.members['participant-1'] = {
|
|
107
|
-
user: { id: 'participant-1', name: 'Alice', is_verified: true },
|
|
108
|
-
user_id: 'participant-1',
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
renderWithProviders(
|
|
112
|
-
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
expect(screen.getByLabelText('Verified')).toBeInTheDocument()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('does not show the verification badge when the participant is not verified', () => {
|
|
119
|
-
const channel = createMockChannel([])
|
|
120
|
-
channel.state.members['participant-1'] = {
|
|
121
|
-
user: { id: 'participant-1', name: 'Alice', is_verified: false },
|
|
122
|
-
user_id: 'participant-1',
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
renderWithProviders(
|
|
126
|
-
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('does not show the verification badge when is_verified is absent', () => {
|
|
133
|
-
const channel = createMockChannel([])
|
|
134
|
-
|
|
135
|
-
renderWithProviders(
|
|
136
|
-
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
expect(screen.queryByLabelText('Verified')).not.toBeInTheDocument()
|
|
140
|
-
})
|
|
141
|
-
|
|
142
117
|
it('shows the latest non-system message when the last message is a system message', () => {
|
|
143
118
|
const channel = createMockChannel([
|
|
144
119
|
{
|
|
@@ -291,6 +266,21 @@ describe('CustomChannelPreview', () => {
|
|
|
291
266
|
expect(screen.getByText('No messages yet')).toBeInTheDocument()
|
|
292
267
|
})
|
|
293
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
|
+
|
|
294
284
|
it('updates starred state when the channel membership changes', () => {
|
|
295
285
|
const channel = createMockChannel([])
|
|
296
286
|
|
|
@@ -299,15 +289,37 @@ describe('CustomChannelPreview', () => {
|
|
|
299
289
|
)
|
|
300
290
|
|
|
301
291
|
expect(
|
|
302
|
-
screen.queryByRole('
|
|
292
|
+
screen.queryByRole('img', { name: 'Starred conversation' })
|
|
303
293
|
).not.toBeInTheDocument()
|
|
304
294
|
|
|
305
295
|
act(() => {
|
|
306
|
-
channel.emitMemberUpdated({
|
|
296
|
+
channel.emitMemberUpdated({
|
|
297
|
+
pinned_at: new Date().toISOString(),
|
|
298
|
+
user_id: mockUser.id,
|
|
299
|
+
})
|
|
307
300
|
})
|
|
308
301
|
|
|
309
302
|
expect(
|
|
310
|
-
screen.getByRole('
|
|
303
|
+
screen.getByRole('img', { name: 'Starred conversation' })
|
|
311
304
|
).toBeInTheDocument()
|
|
312
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('img', { name: 'Starred conversation' })
|
|
323
|
+
).not.toBeInTheDocument()
|
|
324
|
+
})
|
|
313
325
|
})
|
|
@@ -2,13 +2,12 @@ import classNames from 'classnames'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
4
4
|
|
|
5
|
-
import { useChannelStar } from '../../hooks/useChannelStar'
|
|
6
5
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
7
6
|
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
|
|
8
7
|
import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
|
|
9
8
|
import { Avatar } from '../Avatar'
|
|
10
9
|
import { isChatbotMessage } from '../CustomMessage/MessageTag'
|
|
11
|
-
import {
|
|
10
|
+
import { ParticipantBadges } from '../ParticipantBadges'
|
|
12
11
|
|
|
13
12
|
import { useChannelListContext } from './ChannelListContext'
|
|
14
13
|
|
|
@@ -49,7 +48,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
49
48
|
)
|
|
50
49
|
const participantName = resolveParticipantDisplayName(participant?.user)
|
|
51
50
|
const participantImage = participant?.user?.image
|
|
52
|
-
const isParticipantVerified = participant?.user?.is_verified === true
|
|
53
51
|
|
|
54
52
|
// Get last non-system message for preview
|
|
55
53
|
const lastMessage = (() => {
|
|
@@ -71,11 +69,13 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
71
69
|
if (isTip) return displayText ? `💵 ${displayText}` : '💵 Sent a tip'
|
|
72
70
|
const isPaid = lastMessage?.metadata?.custom_type === 'MESSAGE_PAID'
|
|
73
71
|
if (isPaid) return displayText ? `💰 ${displayText}` : '💰 Sent a message'
|
|
74
|
-
const isPaidAttachment =
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
const isPaidAttachment =
|
|
73
|
+
lastMessage?.metadata?.custom_type === 'MESSAGE_ATTACHMENT'
|
|
74
|
+
if (isPaidAttachment)
|
|
75
|
+
return displayText ? `📎 ${displayText}` : '📎 Sent an attachment'
|
|
76
|
+
|
|
77
77
|
if (displayText) return displayText
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
const attachment = lastMessage?.attachments?.[0]
|
|
80
80
|
if (attachment) {
|
|
81
81
|
// Link previews - show the actual URL
|
|
@@ -105,7 +105,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
105
105
|
const messagePreview = renderMessagePreview
|
|
106
106
|
? renderMessagePreview(lastMessage, lastMessageText)
|
|
107
107
|
: `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
|
|
108
|
-
const isChannelStarred = useChannelStar(channel)
|
|
109
108
|
|
|
110
109
|
// Use the unread prop passed by Stream Chat (reactive and updates automatically)
|
|
111
110
|
const unreadCount = unread ?? 0
|
|
@@ -133,6 +132,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
133
132
|
'hover:bg-black/[0.02]': !isSelected,
|
|
134
133
|
}
|
|
135
134
|
)}
|
|
135
|
+
data-dd-privacy="mask"
|
|
136
136
|
>
|
|
137
137
|
<div className="flex items-start gap-4">
|
|
138
138
|
{/* Avatar */}
|
|
@@ -141,7 +141,6 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
141
141
|
name={participantName}
|
|
142
142
|
image={participantImage}
|
|
143
143
|
size={48}
|
|
144
|
-
starred={isChannelStarred}
|
|
145
144
|
/>
|
|
146
145
|
|
|
147
146
|
{/* Content column */}
|
|
@@ -149,11 +148,11 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
149
148
|
{/* Name and timestamp row */}
|
|
150
149
|
<div className="flex items-center justify-between gap-2">
|
|
151
150
|
<h3 className="flex min-w-0 items-center gap-1 text-sm font-medium text-[#191918]">
|
|
152
|
-
{isChannelStarred && (
|
|
153
|
-
<span className="sr-only">Starred conversation. </span>
|
|
154
|
-
)}
|
|
155
151
|
<span className="min-w-0 truncate">{participantName}</span>
|
|
156
|
-
|
|
152
|
+
<ParticipantBadges
|
|
153
|
+
channel={channel}
|
|
154
|
+
participant={participant}
|
|
155
|
+
/>
|
|
157
156
|
</h3>
|
|
158
157
|
{lastMessageTime && (
|
|
159
158
|
<span className="text-xs text-[#717070] flex-shrink-0">
|
|
@@ -7,7 +7,8 @@ import { renderWithProviders } from '../../test/utils'
|
|
|
7
7
|
import { ChannelList } from '.'
|
|
8
8
|
|
|
9
9
|
const streamChannelListMock = vi.fn()
|
|
10
|
-
const { InfiniteScrollMock } = vi.hoisted(() => ({
|
|
10
|
+
const { CustomChannelPreviewMock, InfiniteScrollMock } = vi.hoisted(() => ({
|
|
11
|
+
CustomChannelPreviewMock: () => null,
|
|
11
12
|
InfiniteScrollMock: () => null,
|
|
12
13
|
}))
|
|
13
14
|
|
|
@@ -24,7 +25,7 @@ vi.mock('../../providers/MessagingProvider', () => ({
|
|
|
24
25
|
}))
|
|
25
26
|
|
|
26
27
|
vi.mock('./CustomChannelPreview', () => ({
|
|
27
|
-
default:
|
|
28
|
+
default: CustomChannelPreviewMock,
|
|
28
29
|
}))
|
|
29
30
|
|
|
30
31
|
describe('ChannelList', () => {
|
|
@@ -55,6 +56,28 @@ describe('ChannelList', () => {
|
|
|
55
56
|
})
|
|
56
57
|
})
|
|
57
58
|
|
|
59
|
+
it('uses the built-in channel preview by default', () => {
|
|
60
|
+
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
61
|
+
|
|
62
|
+
expect(streamChannelListMock).toHaveBeenCalledOnce()
|
|
63
|
+
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
64
|
+
Preview: CustomChannelPreviewMock,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('passes a custom channel preview through to Stream ChannelList', () => {
|
|
69
|
+
const ChannelPreview = () => null
|
|
70
|
+
|
|
71
|
+
renderWithProviders(
|
|
72
|
+
<ChannelList {...defaultProps} channelPreview={ChannelPreview} />
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
expect(streamChannelListMock).toHaveBeenCalledOnce()
|
|
76
|
+
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
77
|
+
Preview: ChannelPreview,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
58
81
|
it('passes allowNewMessagesFromUnfilteredChannels through to Stream ChannelList', () => {
|
|
59
82
|
const props: React.ComponentProps<typeof ChannelList> = {
|
|
60
83
|
...defaultProps,
|
|
@@ -28,6 +28,7 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
28
28
|
sort = DEFAULT_SORT,
|
|
29
29
|
className,
|
|
30
30
|
customEmptyStateIndicator,
|
|
31
|
+
channelPreview,
|
|
31
32
|
renderMessagePreview,
|
|
32
33
|
viewerLanguage,
|
|
33
34
|
}) => {
|
|
@@ -97,7 +98,7 @@ export const ChannelList = React.memo<ChannelListProps>(
|
|
|
97
98
|
}
|
|
98
99
|
channelRenderFilterFn={wrappedChannelRenderFilterFn}
|
|
99
100
|
Paginator={InfiniteScroll}
|
|
100
|
-
Preview={CustomChannelPreview}
|
|
101
|
+
Preview={channelPreview ?? CustomChannelPreview}
|
|
101
102
|
EmptyStateIndicator={customEmptyStateIndicator}
|
|
102
103
|
/>
|
|
103
104
|
</ChannelListProvider>
|