@linktr.ee/messaging-react 2.5.3 → 2.6.0-rc-1780281100
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-BbMWytg6.cjs → Card-BHrhU1uR.cjs} +2 -2
- package/dist/{Card-BbMWytg6.cjs.map → Card-BHrhU1uR.cjs.map} +1 -1
- package/dist/{Card-B7TJ2lbG.cjs → Card-BdyCnBOJ.cjs} +2 -2
- package/dist/{Card-B7TJ2lbG.cjs.map → Card-BdyCnBOJ.cjs.map} +1 -1
- package/dist/{Card-CJDGgG0O.js → Card-CD9KC9WP.js} +3 -3
- package/dist/{Card-CJDGgG0O.js.map → Card-CD9KC9WP.js.map} +1 -1
- package/dist/{Card-CruyYUqd.cjs → Card-CJMJGeFI.cjs} +2 -2
- package/dist/{Card-CruyYUqd.cjs.map → Card-CJMJGeFI.cjs.map} +1 -1
- package/dist/{Card-BwygcpzI.js → Card-CWjGaRH2.js} +2 -2
- package/dist/{Card-BwygcpzI.js.map → Card-CWjGaRH2.js.map} +1 -1
- package/dist/{Card-Dy66r68A.js → Card-CbDg3PRA.js} +2 -2
- package/dist/{Card-Dy66r68A.js.map → Card-CbDg3PRA.js.map} +1 -1
- package/dist/{LockedThumbnail-BJxXEcxk.js → LockedThumbnail-DzSYmmYA.js} +2 -2
- package/dist/{LockedThumbnail-BJxXEcxk.js.map → LockedThumbnail-DzSYmmYA.js.map} +1 -1
- package/dist/{LockedThumbnail-DrsPt5LN.cjs → LockedThumbnail-ineY4nwg.cjs} +2 -2
- package/dist/{LockedThumbnail-DrsPt5LN.cjs.map → LockedThumbnail-ineY4nwg.cjs.map} +1 -1
- package/dist/index-Be5pkdgH.cjs +2 -0
- package/dist/index-Be5pkdgH.cjs.map +1 -0
- package/dist/{index-CPa6Qru7.js → index-n6fP9phm.js} +1385 -1371
- package/dist/index-n6fP9phm.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ChannelView.tsx +7 -0
- package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +126 -30
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +2 -13
- package/src/components/CustomMessageInput/index.tsx +51 -45
- package/src/components/MessagingShell/index.tsx +4 -0
- package/src/types.ts +10 -0
- package/dist/index-BIInWpum.cjs +0 -2
- package/dist/index-BIInWpum.cjs.map +0 -1
- package/dist/index-CPa6Qru7.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-Be5pkdgH.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
|
@@ -162,7 +162,7 @@ export declare const ChannelView: default_2.NamedExoticComponent<ChannelViewProp
|
|
|
162
162
|
* Props that MessagingShell passes through to ChannelView.
|
|
163
163
|
* ChannelViewProps is the source of truth for these props.
|
|
164
164
|
*/
|
|
165
|
-
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'onMessageLinkClick' | 'sendButton'>;
|
|
165
|
+
declare type ChannelViewPassthroughProps = Pick<ChannelViewProps, 'renderMessageInputActions' | 'renderMessageInputFooter' | 'renderConversationFooter' | 'CustomChannelEmptyState' | 'onDeleteConversationClick' | 'onBlockParticipantClick' | 'onReportParticipantClick' | 'dmAgentEnabled' | 'messageMetadata' | 'onMessageSent' | 'showStarButton' | 'chatbotVotingEnabled' | 'viewerLanguage' | 'renderChannelBanner' | 'customProfileContent' | 'customChannelActions' | 'renderMessage' | 'onMessageLinkClick' | 'sendButton' | 'attachmentPreviewList'>;
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* ChannelView component props
|
|
@@ -172,6 +172,7 @@ export declare interface ChannelViewProps {
|
|
|
172
172
|
onBack?: () => void;
|
|
173
173
|
showBackButton?: boolean;
|
|
174
174
|
renderMessageInputActions?: (channel: Channel) => React.ReactNode;
|
|
175
|
+
renderMessageInputFooter?: (channel: Channel) => React.ReactNode;
|
|
175
176
|
renderConversationFooter?: (channel: Channel) => React.ReactNode;
|
|
176
177
|
onLeaveConversation?: (channel: Channel) => void;
|
|
177
178
|
onBlockParticipant?: (participantId?: string) => void;
|
|
@@ -298,6 +299,12 @@ export declare interface ChannelViewProps {
|
|
|
298
299
|
* sendButton={MediaSendButton}
|
|
299
300
|
*/
|
|
300
301
|
sendButton?: ComponentType<any>;
|
|
302
|
+
/**
|
|
303
|
+
* Custom component rendered inside the message composer bubble (above the
|
|
304
|
+
* textarea). Passed to Stream `Channel` as `AttachmentPreviewList`.
|
|
305
|
+
* Use this to render staged paid or media attachments inside the input.
|
|
306
|
+
*/
|
|
307
|
+
attachmentPreviewList?: ComponentType;
|
|
301
308
|
}
|
|
302
309
|
|
|
303
310
|
export declare interface ComposerCardProps extends LockedAttachmentBaseProps {
|
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-n6fP9phm.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -254,6 +254,7 @@ const ChannelViewInner: React.FC<{
|
|
|
254
254
|
onBack?: () => void
|
|
255
255
|
showBackButton: boolean
|
|
256
256
|
renderMessageInputActions?: (channel: ChannelType) => React.ReactNode
|
|
257
|
+
renderMessageInputFooter?: (channel: ChannelType) => React.ReactNode
|
|
257
258
|
renderConversationFooter?: (channel: ChannelType) => React.ReactNode
|
|
258
259
|
onLeaveConversation?: (channel: ChannelType) => void
|
|
259
260
|
onBlockParticipant?: (participantId?: string) => void
|
|
@@ -280,6 +281,7 @@ const ChannelViewInner: React.FC<{
|
|
|
280
281
|
onBack,
|
|
281
282
|
showBackButton,
|
|
282
283
|
renderMessageInputActions,
|
|
284
|
+
renderMessageInputFooter,
|
|
283
285
|
renderConversationFooter,
|
|
284
286
|
onLeaveConversation,
|
|
285
287
|
onBlockParticipant,
|
|
@@ -430,6 +432,7 @@ const ChannelViewInner: React.FC<{
|
|
|
430
432
|
<CustomMessageInput
|
|
431
433
|
key="lt-channel-message-input"
|
|
432
434
|
renderActions={() => renderMessageInputActions?.(channel)}
|
|
435
|
+
renderFooter={() => renderMessageInputFooter?.(channel)}
|
|
433
436
|
/>
|
|
434
437
|
</Window>
|
|
435
438
|
</WithComponents>
|
|
@@ -464,6 +467,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
464
467
|
onBack,
|
|
465
468
|
showBackButton = false,
|
|
466
469
|
renderMessageInputActions,
|
|
470
|
+
renderMessageInputFooter,
|
|
467
471
|
renderConversationFooter,
|
|
468
472
|
onLeaveConversation,
|
|
469
473
|
onBlockParticipant,
|
|
@@ -484,6 +488,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
484
488
|
renderMessage,
|
|
485
489
|
onMessageLinkClick,
|
|
486
490
|
sendButton,
|
|
491
|
+
attachmentPreviewList,
|
|
487
492
|
viewerLanguage,
|
|
488
493
|
getParticipantDisplayName: getParticipantDisplayNameProp,
|
|
489
494
|
}) => {
|
|
@@ -577,11 +582,13 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
577
582
|
TypingIndicator={CustomTypingIndicator}
|
|
578
583
|
doSendMessageRequest={doSendMessageRequest}
|
|
579
584
|
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
585
|
+
{...(attachmentPreviewList ? { AttachmentPreviewList: attachmentPreviewList } : {})}
|
|
580
586
|
>
|
|
581
587
|
<ChannelViewInner
|
|
582
588
|
onBack={onBack}
|
|
583
589
|
showBackButton={showBackButton}
|
|
584
590
|
renderMessageInputActions={renderMessageInputActions}
|
|
591
|
+
renderMessageInputFooter={renderMessageInputFooter}
|
|
585
592
|
renderConversationFooter={renderConversationFooter}
|
|
586
593
|
onLeaveConversation={onLeaveConversation}
|
|
587
594
|
onBlockParticipant={onBlockParticipant}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { PlusIcon, XIcon } from '@phosphor-icons/react'
|
|
1
2
|
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
3
|
import React, { useEffect } from 'react'
|
|
3
4
|
import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
|
|
4
|
-
import { Channel, Chat } from 'stream-chat-react'
|
|
5
|
+
import { Channel, Chat, type SendButtonProps } from 'stream-chat-react'
|
|
6
|
+
|
|
7
|
+
import LockedAttachment from '../LockedAttachment'
|
|
5
8
|
|
|
6
9
|
import { CustomMessageInput } from '.'
|
|
7
10
|
|
|
@@ -25,20 +28,37 @@ const createMockChannel = async (
|
|
|
25
28
|
client: StreamChat,
|
|
26
29
|
opts: { frozen?: boolean } = {}
|
|
27
30
|
) => {
|
|
28
|
-
const channelData = {
|
|
31
|
+
const channelData = {
|
|
32
|
+
members: [mockUser.id, mockParticipant.id],
|
|
33
|
+
frozen: opts.frozen ?? false,
|
|
34
|
+
}
|
|
29
35
|
const ch = client.channel('messaging', 'storybook-cmi-channel', channelData)
|
|
30
36
|
|
|
31
37
|
ch.watch = async () => {
|
|
32
38
|
ch.state.members = {
|
|
33
39
|
[mockUser.id]: { user: mockUser, user_id: mockUser.id },
|
|
34
|
-
[mockParticipant.id]: {
|
|
40
|
+
[mockParticipant.id]: {
|
|
41
|
+
user: mockParticipant,
|
|
42
|
+
user_id: mockParticipant.id,
|
|
43
|
+
},
|
|
35
44
|
}
|
|
36
45
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
46
|
;(ch as any)._data = channelData
|
|
38
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
channel: channelData,
|
|
49
|
+
members: [],
|
|
50
|
+
messages: [],
|
|
51
|
+
watchers: [],
|
|
52
|
+
pinned_messages: [],
|
|
53
|
+
duration: '0ms',
|
|
54
|
+
} as unknown as QueryChannelAPIResponse
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
try {
|
|
57
|
+
try {
|
|
58
|
+
await ch.watch()
|
|
59
|
+
} catch (_) {
|
|
60
|
+
/* mock */
|
|
61
|
+
}
|
|
42
62
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
63
|
;(ch as any)._data = channelData
|
|
44
64
|
return ch
|
|
@@ -50,15 +70,15 @@ const createMockChannel = async (
|
|
|
50
70
|
|
|
51
71
|
type WrapperProps = {
|
|
52
72
|
frozen?: boolean
|
|
53
|
-
sendButton?: React.ComponentType<{
|
|
54
|
-
sendMessage: (...args: unknown[]) => unknown
|
|
55
|
-
disabled?: boolean
|
|
56
|
-
[key: string]: unknown
|
|
57
|
-
}>
|
|
58
73
|
renderActions?: () => React.ReactNode
|
|
74
|
+
sendButton?: React.ComponentType<SendButtonProps>
|
|
75
|
+
attachmentPreviewList?: React.ComponentType
|
|
76
|
+
renderFooter?: () => React.ReactNode
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
const Wrapper: React.FC<WrapperProps> = (
|
|
79
|
+
const Wrapper: React.FC<WrapperProps> = (props) => {
|
|
80
|
+
const { frozen, renderActions, sendButton, attachmentPreviewList, renderFooter } = props
|
|
81
|
+
|
|
62
82
|
const [client] = React.useState(() => {
|
|
63
83
|
const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
|
|
64
84
|
c.userID = mockUser.id
|
|
@@ -79,9 +99,10 @@ const Wrapper: React.FC<WrapperProps> = ({ frozen, sendButton, renderActions })
|
|
|
79
99
|
<Channel
|
|
80
100
|
channel={channel}
|
|
81
101
|
{...(sendButton ? { SendButton: sendButton } : {})}
|
|
102
|
+
{...(attachmentPreviewList ? { AttachmentPreviewList: attachmentPreviewList } : {})}
|
|
82
103
|
>
|
|
83
104
|
<div className="bg-white" style={{ minWidth: 360 }}>
|
|
84
|
-
<CustomMessageInput renderActions={renderActions} />
|
|
105
|
+
<CustomMessageInput renderActions={renderActions} renderFooter={renderFooter} />
|
|
85
106
|
</div>
|
|
86
107
|
</Channel>
|
|
87
108
|
</Chat>
|
|
@@ -150,23 +171,22 @@ WithCustomActionButton.parameters = {
|
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
/** A custom send button that turns purple to signal a media attachment is staged. */
|
|
153
|
-
const PurpleMediaSendButton: React.FC<{
|
|
154
|
-
sendMessage
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
174
|
+
const PurpleMediaSendButton: React.FC<SendButtonProps> = (props) => {
|
|
175
|
+
const { sendMessage, disabled, ...rest } = props
|
|
176
|
+
return (
|
|
177
|
+
<button
|
|
178
|
+
{...rest}
|
|
179
|
+
type="button"
|
|
180
|
+
aria-label="Send"
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
onClick={sendMessage}
|
|
183
|
+
className="str-chat__send-button mt-auto flex items-center justify-center shrink-0 rounded-full size-8 bg-[#6D28D9] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white"
|
|
184
|
+
style={{ fontSize: 15 }}
|
|
185
|
+
>
|
|
186
|
+
🚀
|
|
187
|
+
</button>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
170
190
|
|
|
171
191
|
export const WithCustomSendButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
172
192
|
WithCustomSendButton.args = { sendButton: PurpleMediaSendButton }
|
|
@@ -174,7 +194,83 @@ WithCustomSendButton.parameters = {
|
|
|
174
194
|
docs: {
|
|
175
195
|
description: {
|
|
176
196
|
story:
|
|
177
|
-
|
|
197
|
+
"Passes a custom `SendButton` via `Channel`'s `SendButton` prop (which is how `ChannelView`'s `sendButton` prop surfaces it). `CustomMessageInput` reads the button from `useComponentContext` and falls back to the default when none is provided.",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const WithAttachments: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
203
|
+
WithAttachments.args = {
|
|
204
|
+
attachmentPreviewList: () => (
|
|
205
|
+
<div className="relative size-20 shrink-0 overflow-hidden rounded-2xl border border-black/8">
|
|
206
|
+
<img
|
|
207
|
+
src="https://picsum.photos/seed/attachment/160/160"
|
|
208
|
+
alt="Staged attachment"
|
|
209
|
+
className="size-full object-cover"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
),
|
|
213
|
+
renderActions: () => (
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
aria-label="Add attachment"
|
|
217
|
+
onClick={() => console.log('add attachment')}
|
|
218
|
+
className="flex items-center justify-center size-12 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1]"
|
|
219
|
+
>
|
|
220
|
+
<PlusIcon className="size-5" weight="bold" />
|
|
221
|
+
</button>
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
WithAttachments.parameters = {
|
|
225
|
+
docs: {
|
|
226
|
+
description: {
|
|
227
|
+
story:
|
|
228
|
+
"Message input with a regular image attachment staged. In production thumbnails are portaled into `.central-container` by `ComposerPendingMediaPortal`; here we use `Channel`'s `AttachmentPreviewList` override to achieve the same visual placement via the React tree.",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const WithPaidAttachment: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
|
|
234
|
+
WithPaidAttachment.args = {
|
|
235
|
+
attachmentPreviewList: () => (
|
|
236
|
+
<LockedAttachment.Composer
|
|
237
|
+
title="Exclusive Content"
|
|
238
|
+
mimeType="image/jpeg"
|
|
239
|
+
thumbnailUrl="https://picsum.photos/seed/paid/320/180"
|
|
240
|
+
detail="3.2 MB"
|
|
241
|
+
amountText="AU$9.99"
|
|
242
|
+
onDismiss={() => console.log('dismiss paid attachment')}
|
|
243
|
+
onEditClick={() => console.log('edit paid attachment')}
|
|
244
|
+
/>
|
|
245
|
+
),
|
|
246
|
+
renderActions: () => (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
aria-label="Add attachment"
|
|
250
|
+
onClick={() => console.log('add attachment')}
|
|
251
|
+
className="flex items-center justify-center size-12 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1]"
|
|
252
|
+
>
|
|
253
|
+
<PlusIcon className="size-5" weight="bold" />
|
|
254
|
+
</button>
|
|
255
|
+
),
|
|
256
|
+
renderFooter: () => (
|
|
257
|
+
<div className="flex items-center justify-between gap-2 rounded-lg bg-[#E8EEFF] px-4 py-3 text-sm text-black/70">
|
|
258
|
+
<span>Tap to add a price</span>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
aria-label="Dismiss"
|
|
262
|
+
className="flex shrink-0 items-center justify-center text-black/40 hover:text-black/70 bg-black/10 rounded-full p-1"
|
|
263
|
+
>
|
|
264
|
+
<XIcon className="size-4" />
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
WithPaidAttachment.parameters = {
|
|
270
|
+
docs: {
|
|
271
|
+
description: {
|
|
272
|
+
story:
|
|
273
|
+
"Message input with a paid attachment staged inside the input bubble. Uses `Channel`'s `AttachmentPreviewList` override to render a `LockedAttachment.Composer` card at the top of the input. `onDismiss` removes it; `onEditClick` reopens the paid content wizard.",
|
|
178
274
|
},
|
|
179
275
|
},
|
|
180
276
|
}
|
|
@@ -28,9 +28,6 @@ vi.mock('stream-chat-react', () => ({
|
|
|
28
28
|
<Input />
|
|
29
29
|
</div>
|
|
30
30
|
),
|
|
31
|
-
SimpleAttachmentSelector: () => (
|
|
32
|
-
<div data-testid="simple-attachment-selector" />
|
|
33
|
-
),
|
|
34
31
|
TextareaComposer: ({
|
|
35
32
|
maxRows: _maxRows,
|
|
36
33
|
...props
|
|
@@ -84,7 +81,7 @@ describe('CustomMessageInput', () => {
|
|
|
84
81
|
|
|
85
82
|
const { container } = renderWithProviders(<CustomMessageInput />)
|
|
86
83
|
|
|
87
|
-
const messageInput = container.
|
|
84
|
+
const messageInput = container.querySelector('.message-input')
|
|
88
85
|
expect(messageInput).toHaveAttribute('aria-disabled', 'true')
|
|
89
86
|
expect(messageInput).toHaveAttribute('inert')
|
|
90
87
|
expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
|
|
@@ -105,14 +102,6 @@ describe('CustomMessageInput', () => {
|
|
|
105
102
|
expect(sendButton).toBeDisabled()
|
|
106
103
|
})
|
|
107
104
|
|
|
108
|
-
it('renders the existing attachment selector when channel is frozen', () => {
|
|
109
|
-
mockChannelData = { frozen: true }
|
|
110
|
-
|
|
111
|
-
renderWithProviders(<CustomMessageInput />)
|
|
112
|
-
|
|
113
|
-
expect(screen.getByTestId('simple-attachment-selector')).toBeInTheDocument()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
105
|
it('renders adjacent actions inside the disabled container when frozen', () => {
|
|
117
106
|
mockChannelData = { frozen: true }
|
|
118
107
|
|
|
@@ -126,7 +115,7 @@ describe('CustomMessageInput', () => {
|
|
|
126
115
|
/>
|
|
127
116
|
)
|
|
128
117
|
|
|
129
|
-
const messageInput = container.
|
|
118
|
+
const messageInput = container.querySelector('.message-input')
|
|
130
119
|
const action = screen.getByTestId('custom-action')
|
|
131
120
|
|
|
132
121
|
expect(messageInput).toHaveAttribute('aria-disabled', 'true')
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { ArrowUpIcon } from '@phosphor-icons/react'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import {
|
|
4
|
-
AttachmentPreviewList,
|
|
4
|
+
AttachmentPreviewList as DefaultAttachmentPreviewList,
|
|
5
5
|
MessageInput,
|
|
6
6
|
QuotedMessagePreview,
|
|
7
|
-
SimpleAttachmentSelector,
|
|
8
7
|
TextareaComposer,
|
|
9
8
|
useChannelStateContext,
|
|
10
9
|
useComponentContext,
|
|
@@ -32,71 +31,78 @@ const DefaultSendButton: React.FC<{
|
|
|
32
31
|
|
|
33
32
|
const CustomMessageInputInner: React.FC = () => {
|
|
34
33
|
const { channel } = useChannelStateContext()
|
|
35
|
-
const isFrozen = channel?.data?.frozen === true
|
|
36
34
|
const { handleSubmit } = useMessageInputContext()
|
|
37
|
-
|
|
38
|
-
useComponentContext('CustomMessageInput')
|
|
39
|
-
const SendButton = SendButtonFromContext ?? DefaultSendButton
|
|
35
|
+
|
|
40
36
|
const hasSendableData = useMessageComposerHasSendableData()
|
|
37
|
+
const isFrozen = channel?.data?.frozen === true
|
|
41
38
|
const isSendDisabled = isFrozen || !hasSendableData
|
|
42
39
|
|
|
40
|
+
const {
|
|
41
|
+
SendButton = DefaultSendButton,
|
|
42
|
+
AttachmentPreviewList = DefaultAttachmentPreviewList,
|
|
43
|
+
} = useComponentContext('CustomMessageInput')
|
|
44
|
+
|
|
43
45
|
return (
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<div className="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
autoFocus={!isFrozen}
|
|
62
|
-
maxRows={4}
|
|
63
|
-
readOnly={isFrozen}
|
|
64
|
-
tabIndex={isFrozen ? -1 : undefined}
|
|
65
|
-
/>
|
|
66
|
-
</div>
|
|
67
|
-
<SendButton
|
|
68
|
-
sendMessage={handleSubmit}
|
|
69
|
-
aria-label="Send"
|
|
70
|
-
className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
|
|
71
|
-
data-testid="send-button"
|
|
72
|
-
disabled={isSendDisabled}
|
|
73
|
-
type="button"
|
|
46
|
+
<div className="central-container flex flex-col gap-2 min-w-0 w-full p-2 bg-white rounded-[1.5rem] shadow-[0_4px_16px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.04)]">
|
|
47
|
+
<QuotedMessagePreview />
|
|
48
|
+
<CustomLinkPreviewList />
|
|
49
|
+
<AttachmentPreviewList />
|
|
50
|
+
<div className="flex">
|
|
51
|
+
<div className="w-full ml-2 mr-4 self-center leading-[0]">
|
|
52
|
+
<TextareaComposer
|
|
53
|
+
aria-disabled={isFrozen || undefined}
|
|
54
|
+
className="w-full resize-none outline-none leading-6"
|
|
55
|
+
// While this might usually be considered an anti-pattern, in most
|
|
56
|
+
// cases, when a message thread is rendered, we want the input to
|
|
57
|
+
// gain focus automatically.
|
|
58
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
59
|
+
autoFocus={!isFrozen}
|
|
60
|
+
maxRows={4}
|
|
61
|
+
readOnly={isFrozen}
|
|
62
|
+
tabIndex={isFrozen ? -1 : undefined}
|
|
74
63
|
/>
|
|
75
64
|
</div>
|
|
65
|
+
<SendButton
|
|
66
|
+
sendMessage={handleSubmit}
|
|
67
|
+
aria-label="Send"
|
|
68
|
+
className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
|
|
69
|
+
data-testid="send-button"
|
|
70
|
+
disabled={isSendDisabled}
|
|
71
|
+
type="button"
|
|
72
|
+
/>
|
|
76
73
|
</div>
|
|
77
|
-
|
|
74
|
+
</div>
|
|
78
75
|
)
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
export interface CustomMessageInputProps {
|
|
82
79
|
renderActions?: () => React.ReactNode
|
|
80
|
+
renderFooter?: () => React.ReactNode
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
|
|
86
84
|
renderActions,
|
|
85
|
+
renderFooter,
|
|
87
86
|
}) => {
|
|
88
87
|
const { channel } = useChannelStateContext()
|
|
89
88
|
const isFrozen = channel?.data?.frozen === true
|
|
90
89
|
|
|
91
90
|
return (
|
|
92
|
-
<div
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
<div className="flex flex-col gap-4">
|
|
92
|
+
<div
|
|
93
|
+
// @ts-expect-error Only React 19 onwards has `inert` in its types.
|
|
94
|
+
inert={isFrozen ? '' : undefined}
|
|
95
|
+
aria-disabled={isFrozen || undefined}
|
|
96
|
+
className="message-input flex items-end gap-4 aria-disabled:opacity-40"
|
|
97
|
+
>
|
|
98
|
+
{renderActions && (
|
|
99
|
+
<div className="flex h-12 shrink-0 items-center justify-center">
|
|
100
|
+
{renderActions()}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
<MessageInput Input={CustomMessageInputInner} />
|
|
104
|
+
</div>
|
|
105
|
+
{renderFooter?.()}
|
|
100
106
|
</div>
|
|
101
107
|
)
|
|
102
108
|
}
|
|
@@ -18,6 +18,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
18
18
|
capabilities = {},
|
|
19
19
|
className,
|
|
20
20
|
renderMessageInputActions,
|
|
21
|
+
renderMessageInputFooter,
|
|
21
22
|
renderConversationFooter,
|
|
22
23
|
onChannelSelect,
|
|
23
24
|
initialParticipantFilter,
|
|
@@ -43,6 +44,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
43
44
|
renderMessage,
|
|
44
45
|
onMessageLinkClick,
|
|
45
46
|
sendButton,
|
|
47
|
+
attachmentPreviewList,
|
|
46
48
|
}) => {
|
|
47
49
|
const {
|
|
48
50
|
service,
|
|
@@ -404,6 +406,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
404
406
|
onBack={handleBackToChannelList}
|
|
405
407
|
showBackButton={!directConversationMode}
|
|
406
408
|
renderMessageInputActions={renderMessageInputActions}
|
|
409
|
+
renderMessageInputFooter={renderMessageInputFooter}
|
|
407
410
|
renderConversationFooter={renderConversationFooter}
|
|
408
411
|
renderChannelBanner={renderChannelBanner}
|
|
409
412
|
onLeaveConversation={handleLeaveConversation}
|
|
@@ -424,6 +427,7 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
424
427
|
renderMessage={renderMessage}
|
|
425
428
|
onMessageLinkClick={onMessageLinkClick}
|
|
426
429
|
sendButton={sendButton}
|
|
430
|
+
attachmentPreviewList={attachmentPreviewList}
|
|
427
431
|
/>
|
|
428
432
|
</div>
|
|
429
433
|
) : initialParticipantFilter ? (
|
package/src/types.ts
CHANGED
|
@@ -123,6 +123,7 @@ export interface ChannelViewProps {
|
|
|
123
123
|
onBack?: () => void
|
|
124
124
|
showBackButton?: boolean
|
|
125
125
|
renderMessageInputActions?: (channel: Channel) => React.ReactNode
|
|
126
|
+
renderMessageInputFooter?: (channel: Channel) => React.ReactNode
|
|
126
127
|
renderConversationFooter?: (channel: Channel) => React.ReactNode
|
|
127
128
|
onLeaveConversation?: (channel: Channel) => void
|
|
128
129
|
onBlockParticipant?: (participantId?: string) => void
|
|
@@ -266,6 +267,13 @@ export interface ChannelViewProps {
|
|
|
266
267
|
*/
|
|
267
268
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
268
269
|
sendButton?: ComponentType<any>
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Custom component rendered inside the message composer bubble (above the
|
|
273
|
+
* textarea). Passed to Stream `Channel` as `AttachmentPreviewList`.
|
|
274
|
+
* Use this to render staged paid or media attachments inside the input.
|
|
275
|
+
*/
|
|
276
|
+
attachmentPreviewList?: ComponentType
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
/**
|
|
@@ -275,6 +283,7 @@ export interface ChannelViewProps {
|
|
|
275
283
|
export type ChannelViewPassthroughProps = Pick<
|
|
276
284
|
ChannelViewProps,
|
|
277
285
|
| 'renderMessageInputActions'
|
|
286
|
+
| 'renderMessageInputFooter'
|
|
278
287
|
| 'renderConversationFooter'
|
|
279
288
|
| 'CustomChannelEmptyState'
|
|
280
289
|
| 'onDeleteConversationClick'
|
|
@@ -292,6 +301,7 @@ export type ChannelViewPassthroughProps = Pick<
|
|
|
292
301
|
| 'renderMessage'
|
|
293
302
|
| 'onMessageLinkClick'
|
|
294
303
|
| 'sendButton'
|
|
304
|
+
| 'attachmentPreviewList'
|
|
295
305
|
>
|
|
296
306
|
|
|
297
307
|
/**
|