@linktr.ee/messaging-react 3.5.0-rc-1782792813 → 3.5.0-rc-1782793221
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-BnGeMt4f.js → Card-49AvU29H.js} +2 -2
- package/dist/{Card-BnGeMt4f.js.map → Card-49AvU29H.js.map} +1 -1
- package/dist/{Card-DpCwDviq.cjs → Card-Cse19FAO.cjs} +2 -2
- package/dist/{Card-DpCwDviq.cjs.map → Card-Cse19FAO.cjs.map} +1 -1
- package/dist/{Card-qrDaFHtJ.cjs → Card-DBIVUdM5.cjs} +2 -2
- package/dist/{Card-qrDaFHtJ.cjs.map → Card-DBIVUdM5.cjs.map} +1 -1
- package/dist/{Card-BWWBaNKF.js → Card-DS8zNUkL.js} +2 -2
- package/dist/{Card-BWWBaNKF.js.map → Card-DS8zNUkL.js.map} +1 -1
- package/dist/{Card-Bj4YaHav.cjs → Card-DgPx-6aq.cjs} +2 -2
- package/dist/{Card-Bj4YaHav.cjs.map → Card-DgPx-6aq.cjs.map} +1 -1
- package/dist/{Card-DkXzpFLA.js → Card-q0zqEbga.js} +3 -3
- package/dist/{Card-DkXzpFLA.js.map → Card-q0zqEbga.js.map} +1 -1
- package/dist/{LockedThumbnail-DhXEk-NX.cjs → LockedThumbnail-BFgw9F0g.cjs} +2 -2
- package/dist/{LockedThumbnail-DhXEk-NX.cjs.map → LockedThumbnail-BFgw9F0g.cjs.map} +1 -1
- package/dist/{LockedThumbnail-DJR--PAi.js → LockedThumbnail-CRBDhB49.js} +2 -2
- package/dist/{LockedThumbnail-DJR--PAi.js.map → LockedThumbnail-CRBDhB49.js.map} +1 -1
- package/dist/index-BcmwckcI.cjs +2 -0
- package/dist/index-BcmwckcI.cjs.map +1 -0
- package/dist/{index-ubCRSgQk.js → index-DxzkXLSW.js} +1150 -1172
- package/dist/index-DxzkXLSW.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +0 -12
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/ChannelList/ChannelList.stories.tsx +37 -0
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +0 -110
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +8 -58
- package/src/components/ChannelList/CustomChannelPreview.tsx +12 -7
- package/src/components/ChannelView.stories.tsx +7 -38
- package/src/components/ChannelView.test.tsx +27 -44
- package/src/components/ChannelView.tsx +6 -4
- package/src/hooks/useChannelStar.ts +6 -9
- package/src/stream-custom-data.ts +1 -16
- package/dist/index-DzOA7Yu8.cjs +0 -2
- package/dist/index-DzOA7Yu8.cjs.map +0 -1
- package/dist/index-ubCRSgQk.js.map +0 -1
- package/src/components/ParticipantBadges.tsx +0 -42
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-BcmwckcI.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
|
@@ -1501,13 +1501,6 @@ declare module 'stream-chat' {
|
|
|
1501
1501
|
*/
|
|
1502
1502
|
chatbot_paused?: boolean;
|
|
1503
1503
|
}
|
|
1504
|
-
interface CustomMemberData {
|
|
1505
|
-
/**
|
|
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.
|
|
1508
|
-
*/
|
|
1509
|
-
customer?: boolean;
|
|
1510
|
-
}
|
|
1511
1504
|
interface CustomMessageData {
|
|
1512
1505
|
/**
|
|
1513
1506
|
* When true, hides the date timestamp in system messages.
|
|
@@ -1525,10 +1518,5 @@ declare module 'stream-chat' {
|
|
|
1525
1518
|
*/
|
|
1526
1519
|
dm_agent_system_type?: DmAgentSystemType;
|
|
1527
1520
|
}
|
|
1528
|
-
interface CustomUserData {
|
|
1529
|
-
is_account?: boolean;
|
|
1530
|
-
is_follower?: boolean;
|
|
1531
|
-
username?: string;
|
|
1532
|
-
}
|
|
1533
1521
|
}
|
|
1534
1522
|
|
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 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-
|
|
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-DxzkXLSW.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.0-rc-
|
|
3
|
+
"version": "3.5.0-rc-1782793221",
|
|
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-1782793221",
|
|
42
42
|
"@phosphor-icons/react": "^2.1.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import type { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
3
4
|
|
|
4
5
|
import { MockChatProviderWithChannels } from '../../stories/mocks'
|
|
5
6
|
|
|
@@ -37,6 +38,35 @@ const Template: StoryFn<ComponentProps> = (args) => {
|
|
|
37
38
|
)
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
const HostChannelPreview: React.FC<ChannelPreviewUIComponentProps> = ({
|
|
42
|
+
channel,
|
|
43
|
+
unread,
|
|
44
|
+
}) => {
|
|
45
|
+
const participant = Object.values(channel?.state?.members || {}).find(
|
|
46
|
+
(member) => member.user?.id !== channel?._client?.userID
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<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)]">
|
|
51
|
+
<div className="flex items-center justify-between">
|
|
52
|
+
<div className="min-w-0">
|
|
53
|
+
<p className="truncate text-sm font-semibold text-[#191918]">
|
|
54
|
+
{participant?.user?.name ?? 'Unknown visitor'}
|
|
55
|
+
</p>
|
|
56
|
+
<p className="mt-1 line-clamp-1 text-xs text-[#717070]">
|
|
57
|
+
Host-owned channel preview
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
{Boolean(unread) && (
|
|
61
|
+
<span className="ml-3 rounded-full bg-[#7f22fe] px-2 py-1 text-xs font-medium text-white">
|
|
62
|
+
{unread}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
41
71
|
Default.args = {
|
|
42
72
|
filters: { type: 'messaging' },
|
|
@@ -49,4 +79,11 @@ WithSelection.args = {
|
|
|
49
79
|
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
50
80
|
}
|
|
51
81
|
|
|
82
|
+
export const Custom: StoryFn<ComponentProps> = Template.bind({})
|
|
83
|
+
Custom.args = {
|
|
84
|
+
filters: { type: 'messaging' },
|
|
85
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
86
|
+
channelPreview: HostChannelPreview,
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
LocalMessage,
|
|
7
7
|
StreamChat,
|
|
8
8
|
} from 'stream-chat'
|
|
9
|
-
import type { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
10
9
|
|
|
11
10
|
import {
|
|
12
11
|
daysAgo,
|
|
@@ -15,12 +14,9 @@ import {
|
|
|
15
14
|
now,
|
|
16
15
|
secondsAgo,
|
|
17
16
|
} from '../../stories/decorators/storyTime'
|
|
18
|
-
import { MockChatProviderWithChannels } from '../../stories/mocks'
|
|
19
17
|
|
|
20
18
|
import CustomChannelPreview from './CustomChannelPreview'
|
|
21
19
|
|
|
22
|
-
import { ChannelList } from '.'
|
|
23
|
-
|
|
24
20
|
type ComponentProps = React.ComponentProps<typeof CustomChannelPreview>
|
|
25
21
|
|
|
26
22
|
const meta: Meta<ComponentProps> = {
|
|
@@ -59,8 +55,6 @@ const createMockChannel = (options: {
|
|
|
59
55
|
thumb_url?: string
|
|
60
56
|
}>
|
|
61
57
|
lastMessageMetadata?: CustomMessageData['metadata']
|
|
62
|
-
isCustomer?: boolean
|
|
63
|
-
isStarred?: boolean
|
|
64
58
|
}): Channel => {
|
|
65
59
|
const {
|
|
66
60
|
id,
|
|
@@ -72,8 +66,6 @@ const createMockChannel = (options: {
|
|
|
72
66
|
unreadCount = 0,
|
|
73
67
|
lastMessageAttachments,
|
|
74
68
|
lastMessageMetadata,
|
|
75
|
-
isCustomer = false,
|
|
76
|
-
isStarred = false,
|
|
77
69
|
} = options
|
|
78
70
|
|
|
79
71
|
const eventListeners = new Map<string, Set<(...args: unknown[]) => void>>()
|
|
@@ -97,12 +89,8 @@ const createMockChannel = (options: {
|
|
|
97
89
|
image: participantImage,
|
|
98
90
|
},
|
|
99
91
|
user_id: participantId,
|
|
100
|
-
...(isCustomer ? { customer: true } : {}),
|
|
101
92
|
},
|
|
102
93
|
},
|
|
103
|
-
membership: {
|
|
104
|
-
pinned_at: isStarred ? now().toISOString() : null,
|
|
105
|
-
},
|
|
106
94
|
messages:
|
|
107
95
|
lastMessageText || lastMessageAttachments || lastMessageMetadata
|
|
108
96
|
? ([
|
|
@@ -261,61 +249,6 @@ ChatbotMessage.args = {
|
|
|
261
249
|
}),
|
|
262
250
|
}
|
|
263
251
|
|
|
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
|
-
|
|
319
252
|
export const TipMessage: StoryFn<ComponentProps> = Template.bind({})
|
|
320
253
|
TipMessage.args = {
|
|
321
254
|
channel: createMockChannel({
|
|
@@ -866,46 +799,3 @@ WithLongAttachmentUrl.args = {
|
|
|
866
799
|
}),
|
|
867
800
|
}
|
|
868
801
|
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,15 +12,12 @@ const mockUser = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
type MockChannel = Channel & {
|
|
15
|
-
emitMemberUpdated: (member?: {
|
|
16
|
-
pinned_at?: string | null
|
|
17
|
-
user_id?: string
|
|
18
|
-
}) => void
|
|
15
|
+
emitMemberUpdated: (member?: { pinned_at?: string | null }) => void
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
|
|
22
19
|
const memberUpdatedListeners = new Set<
|
|
23
|
-
(event: { member?: { pinned_at?: string | null
|
|
20
|
+
(event: { member?: { pinned_at?: string | null } }) => void
|
|
24
21
|
>()
|
|
25
22
|
|
|
26
23
|
return {
|
|
@@ -41,34 +38,24 @@ const createMockChannel = (messages: Partial<LocalMessage>[]): MockChannel => {
|
|
|
41
38
|
on: vi.fn(
|
|
42
39
|
(
|
|
43
40
|
eventName: string,
|
|
44
|
-
listener: (event: {
|
|
45
|
-
member?: { pinned_at?: string | null; user_id?: string }
|
|
46
|
-
}) => void
|
|
41
|
+
listener: (event: { member?: { pinned_at?: string | null } }) => void
|
|
47
42
|
) => {
|
|
48
43
|
if (eventName === 'member.updated') {
|
|
49
44
|
memberUpdatedListeners.add(listener)
|
|
50
45
|
}
|
|
51
|
-
return {
|
|
52
|
-
unsubscribe: () => memberUpdatedListeners.delete(listener),
|
|
53
|
-
}
|
|
54
46
|
}
|
|
55
47
|
),
|
|
56
48
|
off: vi.fn(
|
|
57
49
|
(
|
|
58
50
|
eventName: string,
|
|
59
|
-
listener: (event: {
|
|
60
|
-
member?: { pinned_at?: string | null; user_id?: string }
|
|
61
|
-
}) => void
|
|
51
|
+
listener: (event: { member?: { pinned_at?: string | null } }) => void
|
|
62
52
|
) => {
|
|
63
53
|
if (eventName === 'member.updated') {
|
|
64
54
|
memberUpdatedListeners.delete(listener)
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
57
|
),
|
|
68
|
-
emitMemberUpdated: (member?: {
|
|
69
|
-
pinned_at?: string | null
|
|
70
|
-
user_id?: string
|
|
71
|
-
}) => {
|
|
58
|
+
emitMemberUpdated: (member?: { pinned_at?: string | null }) => {
|
|
72
59
|
memberUpdatedListeners.forEach((listener) => listener({ member }))
|
|
73
60
|
},
|
|
74
61
|
} as unknown as MockChannel
|
|
@@ -266,21 +253,6 @@ describe('CustomChannelPreview', () => {
|
|
|
266
253
|
expect(screen.getByText('No messages yet')).toBeInTheDocument()
|
|
267
254
|
})
|
|
268
255
|
|
|
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
|
-
|
|
284
256
|
it('updates starred state when the channel membership changes', () => {
|
|
285
257
|
const channel = createMockChannel([])
|
|
286
258
|
|
|
@@ -289,37 +261,15 @@ describe('CustomChannelPreview', () => {
|
|
|
289
261
|
)
|
|
290
262
|
|
|
291
263
|
expect(
|
|
292
|
-
screen.queryByRole('
|
|
264
|
+
screen.queryByRole('heading', { name: /starred conversation/i })
|
|
293
265
|
).not.toBeInTheDocument()
|
|
294
266
|
|
|
295
267
|
act(() => {
|
|
296
|
-
channel.emitMemberUpdated({
|
|
297
|
-
pinned_at: new Date().toISOString(),
|
|
298
|
-
user_id: mockUser.id,
|
|
299
|
-
})
|
|
268
|
+
channel.emitMemberUpdated({ pinned_at: new Date().toISOString() })
|
|
300
269
|
})
|
|
301
270
|
|
|
302
271
|
expect(
|
|
303
|
-
screen.getByRole('
|
|
272
|
+
screen.getByRole('heading', { name: /starred conversation/i })
|
|
304
273
|
).toBeInTheDocument()
|
|
305
274
|
})
|
|
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
|
-
})
|
|
325
275
|
})
|
|
@@ -2,12 +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'
|
|
5
6
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
6
7
|
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
|
|
7
8
|
import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
|
|
8
9
|
import { Avatar } from '../Avatar'
|
|
9
10
|
import { isChatbotMessage } from '../CustomMessage/MessageTag'
|
|
10
|
-
import { ParticipantBadges } from '../ParticipantBadges'
|
|
11
11
|
|
|
12
12
|
import { useChannelListContext } from './ChannelListContext'
|
|
13
13
|
|
|
@@ -105,6 +105,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
105
105
|
const messagePreview = renderMessagePreview
|
|
106
106
|
? renderMessagePreview(lastMessage, lastMessageText)
|
|
107
107
|
: `${isLastMessageFromChatbot ? '✨ ' : ''}${lastMessageText}`
|
|
108
|
+
const isChannelStarred = useChannelStar(channel)
|
|
108
109
|
|
|
109
110
|
// Use the unread prop passed by Stream Chat (reactive and updates automatically)
|
|
110
111
|
const unreadCount = unread ?? 0
|
|
@@ -141,18 +142,22 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
141
142
|
name={participantName}
|
|
142
143
|
image={participantImage}
|
|
143
144
|
size={48}
|
|
145
|
+
starred={isChannelStarred}
|
|
144
146
|
/>
|
|
145
147
|
|
|
146
148
|
{/* Content column */}
|
|
147
149
|
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
148
150
|
{/* Name and timestamp row */}
|
|
149
151
|
<div className="flex items-center justify-between gap-2">
|
|
150
|
-
<h3
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
<h3
|
|
153
|
+
className={classNames(
|
|
154
|
+
'text-sm font-medium truncate text-[#191918]'
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{isChannelStarred && (
|
|
158
|
+
<span className="sr-only">Starred conversation. </span>
|
|
159
|
+
)}
|
|
160
|
+
{participantName}
|
|
156
161
|
</h3>
|
|
157
162
|
{lastMessageTime && (
|
|
158
163
|
<span className="text-xs text-[#717070] flex-shrink-0">
|
|
@@ -41,14 +41,7 @@ const createMockChannel = async (
|
|
|
41
41
|
client: StreamChat,
|
|
42
42
|
hasMessages = true,
|
|
43
43
|
followerStatus?: string | boolean,
|
|
44
|
-
isFrozen = false
|
|
45
|
-
{
|
|
46
|
-
isCustomer = false,
|
|
47
|
-
isStarred = false,
|
|
48
|
-
}: {
|
|
49
|
-
isCustomer?: boolean
|
|
50
|
-
isStarred?: boolean
|
|
51
|
-
} = {}
|
|
44
|
+
isFrozen = false
|
|
52
45
|
) => {
|
|
53
46
|
const participant = mockParticipants[0]
|
|
54
47
|
|
|
@@ -149,12 +142,8 @@ const createMockChannel = async (
|
|
|
149
142
|
user_id: participant.id,
|
|
150
143
|
role: 'member',
|
|
151
144
|
is_account: true,
|
|
152
|
-
...(isCustomer ? { customer: true } : {}),
|
|
153
145
|
},
|
|
154
146
|
}
|
|
155
|
-
channel.state.membership = {
|
|
156
|
-
pinned_at: isStarred ? now().toISOString() : null,
|
|
157
|
-
} as ChannelMemberResponse
|
|
158
147
|
return {
|
|
159
148
|
channel: channelData,
|
|
160
149
|
members: [],
|
|
@@ -182,8 +171,6 @@ const createMockChannel = async (
|
|
|
182
171
|
type TemplateProps = ComponentProps & {
|
|
183
172
|
followerStatus?: string | boolean
|
|
184
173
|
isFrozen?: boolean
|
|
185
|
-
isCustomer?: boolean
|
|
186
|
-
isStarred?: boolean
|
|
187
174
|
typingUser?: {
|
|
188
175
|
id: string
|
|
189
176
|
name?: string
|
|
@@ -195,8 +182,6 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
195
182
|
const {
|
|
196
183
|
followerStatus,
|
|
197
184
|
isFrozen = false,
|
|
198
|
-
isCustomer = false,
|
|
199
|
-
isStarred = false,
|
|
200
185
|
typingUser,
|
|
201
186
|
...channelViewProps
|
|
202
187
|
} = args
|
|
@@ -212,13 +197,12 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
212
197
|
const [channel, setChannel] = React.useState<ChannelType | null>(null)
|
|
213
198
|
|
|
214
199
|
useEffect(() => {
|
|
215
|
-
createMockChannel(client, true, followerStatus, isFrozen
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}, [client, followerStatus, isCustomer, isFrozen, isStarred])
|
|
200
|
+
createMockChannel(client, true, followerStatus, isFrozen).then(
|
|
201
|
+
(mockChannel) => {
|
|
202
|
+
setChannel(mockChannel)
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
}, [client, followerStatus, isFrozen])
|
|
222
206
|
|
|
223
207
|
useEffect(() => {
|
|
224
208
|
if (!channel || !typingUser) {
|
|
@@ -325,21 +309,6 @@ WithHeaderTitleBadges.parameters = {
|
|
|
325
309
|
},
|
|
326
310
|
}
|
|
327
311
|
|
|
328
|
-
export const ParticipantBadges: StoryFn<TemplateProps> = Template.bind({})
|
|
329
|
-
ParticipantBadges.args = {
|
|
330
|
-
showBackButton: false,
|
|
331
|
-
isCustomer: true,
|
|
332
|
-
isStarred: true,
|
|
333
|
-
}
|
|
334
|
-
ParticipantBadges.parameters = {
|
|
335
|
-
docs: {
|
|
336
|
-
description: {
|
|
337
|
-
story:
|
|
338
|
-
'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.',
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
}
|
|
342
|
-
|
|
343
312
|
export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
|
|
344
313
|
{}
|
|
345
314
|
)
|
|
@@ -84,9 +84,13 @@ vi.mock('./Avatar', () => ({
|
|
|
84
84
|
},
|
|
85
85
|
}))
|
|
86
86
|
|
|
87
|
+
let mockIsStarred = false
|
|
88
|
+
vi.mock('../hooks/useChannelStar', () => ({
|
|
89
|
+
useChannelStar: () => mockIsStarred,
|
|
90
|
+
}))
|
|
91
|
+
|
|
87
92
|
const createChannel = ({
|
|
88
93
|
currentUserId = 'visitor-1',
|
|
89
|
-
currentUserPinnedAt,
|
|
90
94
|
currentUserRole = 'owner',
|
|
91
95
|
otherUser = {
|
|
92
96
|
id: 'linker-1' as const,
|
|
@@ -94,12 +98,9 @@ const createChannel = ({
|
|
|
94
98
|
username: undefined as string | undefined,
|
|
95
99
|
is_account: true,
|
|
96
100
|
},
|
|
97
|
-
otherMemberCustomer = false,
|
|
98
101
|
}: {
|
|
99
102
|
currentUserId?: 'visitor-1' | 'linker-1'
|
|
100
|
-
currentUserPinnedAt?: string
|
|
101
103
|
currentUserRole?: 'owner' | 'member'
|
|
102
|
-
otherMemberCustomer?: boolean
|
|
103
104
|
otherUser?: {
|
|
104
105
|
id: string
|
|
105
106
|
name?: string
|
|
@@ -132,15 +133,12 @@ const createChannel = ({
|
|
|
132
133
|
otherUser.is_account ?? currentUserId === 'visitor-1',
|
|
133
134
|
},
|
|
134
135
|
role: currentUserRole === 'owner' ? 'member' : 'owner',
|
|
135
|
-
customer: otherMemberCustomer,
|
|
136
136
|
},
|
|
137
137
|
},
|
|
138
|
-
membership: {
|
|
139
|
-
pinned_at: currentUserPinnedAt,
|
|
140
|
-
},
|
|
138
|
+
membership: {},
|
|
141
139
|
messages: [],
|
|
142
140
|
},
|
|
143
|
-
on: vi.fn(
|
|
141
|
+
on: vi.fn(),
|
|
144
142
|
off: vi.fn(),
|
|
145
143
|
sendMessage: vi.fn(),
|
|
146
144
|
hide: vi.fn(),
|
|
@@ -155,6 +153,7 @@ describe('ChannelView', () => {
|
|
|
155
153
|
avatarRenderCalls.length = 0
|
|
156
154
|
channelActionsMenuProps.length = 0
|
|
157
155
|
messageInputProps.length = 0
|
|
156
|
+
mockIsStarred = false
|
|
158
157
|
})
|
|
159
158
|
|
|
160
159
|
const lastActionsMenuProps = () =>
|
|
@@ -308,54 +307,38 @@ describe('ChannelView', () => {
|
|
|
308
307
|
expect(screen.queryByTestId('conversation-footer')).not.toBeInTheDocument()
|
|
309
308
|
})
|
|
310
309
|
|
|
311
|
-
it('does not pass starred to avatar when
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
})
|
|
310
|
+
it('does not pass starred to avatar when showStarButton is false', () => {
|
|
311
|
+
mockIsStarred = true
|
|
312
|
+
const channel = createChannel()
|
|
315
313
|
|
|
316
314
|
renderWithProviders(
|
|
317
|
-
<ChannelView channel={channel} showStarButton={
|
|
315
|
+
<ChannelView channel={channel} showStarButton={false} />
|
|
318
316
|
)
|
|
319
317
|
|
|
320
318
|
expect(avatarRenderCalls.length).toBeGreaterThan(0)
|
|
321
|
-
expect(avatarRenderCalls.every((call) =>
|
|
319
|
+
expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
|
|
322
320
|
})
|
|
323
321
|
|
|
324
|
-
it('
|
|
322
|
+
it('passes starred to avatar when showStarButton is true and channel is starred', () => {
|
|
323
|
+
mockIsStarred = true
|
|
324
|
+
const channel = createChannel()
|
|
325
|
+
|
|
325
326
|
renderWithProviders(
|
|
326
|
-
<ChannelView
|
|
327
|
-
channel={createChannel({ otherMemberCustomer: true })}
|
|
328
|
-
renderHeaderTitleBadges={() => (
|
|
329
|
-
<span data-testid="custom-header-badge">VIP</span>
|
|
330
|
-
)}
|
|
331
|
-
/>
|
|
327
|
+
<ChannelView channel={channel} showStarButton={true} />
|
|
332
328
|
)
|
|
333
329
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
expect(customerBadges.length).toBeGreaterThan(0)
|
|
338
|
-
expect(customBadges.length).toBe(customerBadges.length)
|
|
339
|
-
customerBadges.forEach((badge, index) => {
|
|
340
|
-
expect(
|
|
341
|
-
badge.compareDocumentPosition(customBadges[index]) &
|
|
342
|
-
Node.DOCUMENT_POSITION_FOLLOWING
|
|
343
|
-
).toBeTruthy()
|
|
344
|
-
})
|
|
330
|
+
expect(avatarRenderCalls.length).toBeGreaterThan(0)
|
|
331
|
+
expect(avatarRenderCalls.every((call) => call.starred === true)).toBe(true)
|
|
345
332
|
})
|
|
346
333
|
|
|
347
|
-
it('
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
channel={createChannel({
|
|
351
|
-
currentUserPinnedAt: '2026-01-01T00:00:00.000Z',
|
|
352
|
-
})}
|
|
353
|
-
/>
|
|
354
|
-
)
|
|
334
|
+
it('does not pass starred to avatar by default (showStarButton defaults to false)', () => {
|
|
335
|
+
mockIsStarred = true
|
|
336
|
+
const channel = createChannel()
|
|
355
337
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
).toBeGreaterThan(0)
|
|
338
|
+
renderWithProviders(<ChannelView channel={channel} />)
|
|
339
|
+
|
|
340
|
+
expect(avatarRenderCalls.length).toBeGreaterThan(0)
|
|
341
|
+
expect(avatarRenderCalls.every((call) => call.starred === false)).toBe(true)
|
|
359
342
|
})
|
|
360
343
|
|
|
361
344
|
it('passes dmAgentEnabled to avatar when dmAgentEnabled is true for visitor view', () => {
|