@linktr.ee/messaging-react 3.5.0-rc-1782408966 → 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-iekRe643.js → Card-BWWBaNKF.js} +2 -2
- package/dist/{Card-iekRe643.js.map → Card-BWWBaNKF.js.map} +1 -1
- package/dist/{Card-DJtDI03Y.cjs → Card-Bj4YaHav.cjs} +2 -2
- package/dist/{Card-DJtDI03Y.cjs.map → Card-Bj4YaHav.cjs.map} +1 -1
- package/dist/{Card-CUHEDe8O.js → Card-BnGeMt4f.js} +2 -2
- package/dist/{Card-CUHEDe8O.js.map → Card-BnGeMt4f.js.map} +1 -1
- package/dist/{Card-BNzC_bp8.js → Card-DkXzpFLA.js} +3 -3
- package/dist/{Card-BNzC_bp8.js.map → Card-DkXzpFLA.js.map} +1 -1
- package/dist/{Card-DuMr5KMP.cjs → Card-DpCwDviq.cjs} +2 -2
- package/dist/{Card-DuMr5KMP.cjs.map → Card-DpCwDviq.cjs.map} +1 -1
- package/dist/{Card-DEa50m9p.cjs → Card-qrDaFHtJ.cjs} +2 -2
- package/dist/{Card-DEa50m9p.cjs.map → Card-qrDaFHtJ.cjs.map} +1 -1
- package/dist/{LockedThumbnail-CvpRZFi-.js → LockedThumbnail-DJR--PAi.js} +2 -2
- package/dist/{LockedThumbnail-CvpRZFi-.js.map → LockedThumbnail-DJR--PAi.js.map} +1 -1
- package/dist/{LockedThumbnail-aME0f_ZF.cjs → LockedThumbnail-DhXEk-NX.cjs} +2 -2
- package/dist/{LockedThumbnail-aME0f_ZF.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-BlpDX5fl.js → index-ubCRSgQk.js} +1423 -1328
- package/dist/index-ubCRSgQk.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +40 -16
- package/dist/index.js +19 -19
- 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 +123 -68
- package/src/components/CustomMessage/index.tsx +1 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +34 -0
- package/src/components/CustomMessageInput/index.tsx +27 -2
- package/src/components/MediaMessage/index.tsx +13 -5
- package/src/components/MessagingShell/index.tsx +2 -0
- package/src/components/ParticipantBadges.tsx +42 -0
- package/src/hooks/useChannelStar.ts +9 -6
- package/src/index.ts +1 -2
- package/src/stories/mocks.tsx +30 -5
- package/src/stream-custom-data.ts +12 -5
- package/src/types.ts +27 -1
- package/dist/index-BIWS3Inh.cjs +0 -2
- package/dist/index-BIWS3Inh.cjs.map +0 -1
- package/dist/index-BlpDX5fl.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.
|
|
@@ -149,7 +155,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
|
|
|
149
155
|
* Props that MessagingShell passes through to ChannelView.
|
|
150
156
|
* ChannelViewProps is the source of truth for these props.
|
|
151
157
|
*/
|
|
152
|
-
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo'>;
|
|
158
|
+
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'onMessageSent' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderHeaderTitleBadges' | 'renderChannelBanner' | 'customChannelActions' | 'renderChannelActions' | 'onParticipantNameClick' | 'renderMessage' | 'onMessageLinkClick' | 'showChannelInfo' | 'composerInput'>;
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
161
|
* ChannelView component props
|
|
@@ -351,6 +357,22 @@ export declare interface ChannelViewProps {
|
|
|
351
357
|
* Use this to render staged paid or media attachments inside the input.
|
|
352
358
|
*/
|
|
353
359
|
attachmentPreviewList?: ComponentType;
|
|
360
|
+
/**
|
|
361
|
+
* Replace the entire inner composer — the textarea + send arrangement — with
|
|
362
|
+
* a host-owned component. Passed to Stream's `<MessageInput>` as the `Input`
|
|
363
|
+
* component, so it runs inside the message-input context and reuses the
|
|
364
|
+
* library's send pipeline (`doSendMessageRequest`, message metadata, DM-agent
|
|
365
|
+
* suppression), attachments, and previews via the Stream hooks
|
|
366
|
+
* (`useMessageInputContext`, `useMessageComposerHasSendableData`,
|
|
367
|
+
* `TextareaComposer`, …). Use it when a surface needs a different composer
|
|
368
|
+
* layout (e.g. a full-width textarea above a toolbar row) than the default.
|
|
369
|
+
*
|
|
370
|
+
* The outer wrapper — frozen handling and the `renderMessageInputActions` /
|
|
371
|
+
* `renderMessageInputFooter` slots — is unchanged. Read the exported
|
|
372
|
+
* `useComposerLocked()` hook to honour the frozen state. Defaults to the
|
|
373
|
+
* built-in composer when omitted.
|
|
374
|
+
*/
|
|
375
|
+
composerInput?: ComponentType;
|
|
354
376
|
/**
|
|
355
377
|
* Fired when the participant's name in the channel header is clicked. When
|
|
356
378
|
* provided, the name renders as a button with a trailing caret (an affordance
|
|
@@ -1376,6 +1398,14 @@ export declare interface SentCardProps extends LockedAttachmentBaseProps {
|
|
|
1376
1398
|
onFetchSource?: () => Promise<LockedAttachmentSource | void>;
|
|
1377
1399
|
}
|
|
1378
1400
|
|
|
1401
|
+
/**
|
|
1402
|
+
* Read the composer's locked (frozen-channel) state from inside a custom
|
|
1403
|
+
* composer supplied via `composerInput`. Returns true when the channel is
|
|
1404
|
+
* frozen; mirror the default composer by rendering the textarea read-only and
|
|
1405
|
+
* disabling the send control.
|
|
1406
|
+
*/
|
|
1407
|
+
export declare const useComposerLocked: () => boolean;
|
|
1408
|
+
|
|
1379
1409
|
export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(key: K): CustomMessageRegistry[K];
|
|
1380
1410
|
|
|
1381
1411
|
/**
|
|
@@ -1399,17 +1429,6 @@ declare interface UseMessageVoteResult {
|
|
|
1399
1429
|
*/
|
|
1400
1430
|
export declare const useMessaging: () => MessagingContextValue;
|
|
1401
1431
|
|
|
1402
|
-
/**
|
|
1403
|
-
* Linktree verification badge shown next to the name of a verified profile.
|
|
1404
|
-
* Rendered when a participant's Stream user has `is_verified` set to true.
|
|
1405
|
-
*/
|
|
1406
|
-
export declare const VerifiedBadge: default_2.FC<VerifiedBadgeProps>;
|
|
1407
|
-
|
|
1408
|
-
export declare interface VerifiedBadgeProps {
|
|
1409
|
-
className?: string;
|
|
1410
|
-
size?: number;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
1432
|
declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
1414
1433
|
/** Single video — convenience for the most common case. */
|
|
1415
1434
|
src?: string;
|
|
@@ -1482,12 +1501,12 @@ declare module 'stream-chat' {
|
|
|
1482
1501
|
*/
|
|
1483
1502
|
chatbot_paused?: boolean;
|
|
1484
1503
|
}
|
|
1485
|
-
interface
|
|
1504
|
+
interface CustomMemberData {
|
|
1486
1505
|
/**
|
|
1487
|
-
* Whether the
|
|
1488
|
-
*
|
|
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.
|
|
1489
1508
|
*/
|
|
1490
|
-
|
|
1509
|
+
customer?: boolean;
|
|
1491
1510
|
}
|
|
1492
1511
|
interface CustomMessageData {
|
|
1493
1512
|
/**
|
|
@@ -1506,5 +1525,10 @@ declare module 'stream-chat' {
|
|
|
1506
1525
|
*/
|
|
1507
1526
|
dm_agent_system_type?: DmAgentSystemType;
|
|
1508
1527
|
}
|
|
1528
|
+
interface CustomUserData {
|
|
1529
|
+
is_account?: boolean;
|
|
1530
|
+
is_follower?: boolean;
|
|
1531
|
+
username?: string;
|
|
1532
|
+
}
|
|
1509
1533
|
}
|
|
1510
1534
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
o as ChannelList,
|
|
7
|
+
n as ChannelView,
|
|
8
8
|
m as CustomMessageProvider,
|
|
9
9
|
g as FaqList,
|
|
10
10
|
r as FaqListItem,
|
|
@@ -13,23 +13,23 @@ export {
|
|
|
13
13
|
u as MediaMessage,
|
|
14
14
|
L as MessageAttachment,
|
|
15
15
|
c as MessageVoteButtons,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
p as
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
F as getFileExtensionLabel,
|
|
16
|
+
h as MessagingProvider,
|
|
17
|
+
d as MessagingShell,
|
|
18
|
+
p as buildCompactMetaLabel,
|
|
19
|
+
C as formatFileSize,
|
|
20
|
+
v as formatRelativeTime,
|
|
21
|
+
A as getFileExtensionLabel,
|
|
23
22
|
k as getMessageDisplayText,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,
|
|
33
|
+
D as useMessaging
|
|
34
34
|
};
|
|
35
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">
|