@linktr.ee/messaging-react 3.0.0 → 3.1.0-rc-1780514752
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-Cqld0-Ws.js → Card-0TLA8XHU.js} +3 -3
- package/dist/{Card-Cqld0-Ws.js.map → Card-0TLA8XHU.js.map} +1 -1
- package/dist/{Card-B7ePjYQ6.cjs → Card-B-D_LbnV.cjs} +2 -2
- package/dist/{Card-B7ePjYQ6.cjs.map → Card-B-D_LbnV.cjs.map} +1 -1
- package/dist/{Card-C46z9zz4.js → Card-BaaerKBC.js} +2 -2
- package/dist/{Card-C46z9zz4.js.map → Card-BaaerKBC.js.map} +1 -1
- package/dist/{Card-Cq0x0bbb.cjs → Card-Bfxdewx_.cjs} +2 -2
- package/dist/{Card-Cq0x0bbb.cjs.map → Card-Bfxdewx_.cjs.map} +1 -1
- package/dist/{Card-C-ZIQW_q.js → Card-DZVa2CeI.js} +2 -2
- package/dist/{Card-C-ZIQW_q.js.map → Card-DZVa2CeI.js.map} +1 -1
- package/dist/{Card-Drz28Q-Y.cjs → Card-DbdWDBMe.cjs} +2 -2
- package/dist/{Card-Drz28Q-Y.cjs.map → Card-DbdWDBMe.cjs.map} +1 -1
- package/dist/{LockedThumbnail-D5NHhET2.js → LockedThumbnail-B4gDHeh7.js} +2 -2
- package/dist/{LockedThumbnail-D5NHhET2.js.map → LockedThumbnail-B4gDHeh7.js.map} +1 -1
- package/dist/{LockedThumbnail--h4GTH41.cjs → LockedThumbnail-DkwFwgpU.cjs} +2 -2
- package/dist/{LockedThumbnail--h4GTH41.cjs.map → LockedThumbnail-DkwFwgpU.cjs.map} +1 -1
- package/dist/{index-BUT2yBvJ.js → index-BmCc1-F3.js} +1147 -1118
- package/dist/index-BmCc1-F3.js.map +1 -0
- package/dist/index-Cg-bxSZn.cjs +2 -0
- package/dist/index-Cg-bxSZn.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +30 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +43 -1
- package/src/components/ChannelInfoDialog/index.tsx +55 -37
- package/src/components/ChannelView.stories.tsx +25 -0
- package/src/components/ChannelView.test.tsx +84 -5
- package/src/components/ChannelView.tsx +30 -2
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +42 -0
- package/src/components/CustomMessageInput/index.tsx +38 -12
- package/src/types.ts +35 -0
- package/dist/index-BUT2yBvJ.js.map +0 -1
- package/dist/index-DqNobxVj.cjs +0 -2
- package/dist/index-DqNobxVj.cjs.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-Cg-bxSZn.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
|
@@ -181,6 +181,36 @@ export declare interface ChannelViewProps {
|
|
|
181
181
|
* Analytics callback fired when "Report" is clicked.
|
|
182
182
|
*/
|
|
183
183
|
onReportParticipantClick?: () => void;
|
|
184
|
+
/**
|
|
185
|
+
* Show the "Block"/"Unblock" action in the channel info dialog.
|
|
186
|
+
* Defaults to true. Set false for restricted surfaces such as the
|
|
187
|
+
* Linktree official channel, where blocking is not offered.
|
|
188
|
+
*/
|
|
189
|
+
showBlockParticipant?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Show the "Report" action in the channel info dialog.
|
|
192
|
+
* Defaults to true. Set false for restricted surfaces such as the
|
|
193
|
+
* Linktree official channel, where reporting is not offered.
|
|
194
|
+
*/
|
|
195
|
+
showReportParticipant?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Show the subscription/follower-status label in the channel info dialog
|
|
198
|
+
* profile card. Defaults to true. Set false for restricted surfaces such
|
|
199
|
+
* as the Linktree official channel, where subscription status is hidden.
|
|
200
|
+
*/
|
|
201
|
+
showFollowerStatus?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Lock the message composer (read-only, send disabled). Defaults to false.
|
|
204
|
+
* Combined with the channel's `frozen` flag — either one locks the input.
|
|
205
|
+
* Used by the Linktree official channel, where the composer stays locked
|
|
206
|
+
* until the linker messages Linktree from its public profile.
|
|
207
|
+
*/
|
|
208
|
+
composerDisabled?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Explanatory text rendered below the composer while it is locked.
|
|
211
|
+
* Only shown when `composerDisabled` is true (or the channel is frozen).
|
|
212
|
+
*/
|
|
213
|
+
composerDisabledReason?: string;
|
|
184
214
|
/**
|
|
185
215
|
* When true and DM agent is active on the channel (not paused),
|
|
186
216
|
* messages will be sent with skip_push and silent flags to suppress
|
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-BmCc1-F3.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -6,10 +6,14 @@ import { renderWithProviders, screen, userEvent, waitFor } from '../../test/util
|
|
|
6
6
|
|
|
7
7
|
import { ChannelInfoDialog } from './index'
|
|
8
8
|
|
|
9
|
+
const { getBlockedUsersMock } = vi.hoisted(() => ({
|
|
10
|
+
getBlockedUsersMock: vi.fn().mockResolvedValue([]),
|
|
11
|
+
}))
|
|
12
|
+
|
|
9
13
|
vi.mock('../../providers/MessagingProvider', () => ({
|
|
10
14
|
useMessagingContext: () => ({
|
|
11
15
|
service: {
|
|
12
|
-
getBlockedUsers:
|
|
16
|
+
getBlockedUsers: getBlockedUsersMock,
|
|
13
17
|
blockUser: vi.fn().mockResolvedValue(undefined),
|
|
14
18
|
unBlockUser: vi.fn().mockResolvedValue(undefined),
|
|
15
19
|
},
|
|
@@ -278,6 +282,44 @@ describe('ChannelInfoDialog', () => {
|
|
|
278
282
|
windowOpenSpy.mockRestore()
|
|
279
283
|
})
|
|
280
284
|
|
|
285
|
+
it('shows Block and Report actions by default', () => {
|
|
286
|
+
renderWithProviders(<ChannelInfoDialog {...defaultProps()} />)
|
|
287
|
+
|
|
288
|
+
expect(screen.getByText('Block')).toBeInTheDocument()
|
|
289
|
+
expect(screen.getByText('Report')).toBeInTheDocument()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('hides the Block action when showBlockParticipant is false', () => {
|
|
293
|
+
renderWithProviders(
|
|
294
|
+
<ChannelInfoDialog {...defaultProps()} showBlockParticipant={false} />
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
expect(screen.queryByText('Block')).not.toBeInTheDocument()
|
|
298
|
+
expect(screen.queryByText('Unblock')).not.toBeInTheDocument()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('fetches blocked users by default (Block action shown)', () => {
|
|
302
|
+
renderWithProviders(<ChannelInfoDialog {...defaultProps()} />)
|
|
303
|
+
|
|
304
|
+
expect(getBlockedUsersMock).toHaveBeenCalled()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('does not fetch blocked users when showBlockParticipant is false', () => {
|
|
308
|
+
renderWithProviders(
|
|
309
|
+
<ChannelInfoDialog {...defaultProps()} showBlockParticipant={false} />
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
expect(getBlockedUsersMock).not.toHaveBeenCalled()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('hides the Report action when showReportParticipant is false', () => {
|
|
316
|
+
renderWithProviders(
|
|
317
|
+
<ChannelInfoDialog {...defaultProps()} showReportParticipant={false} />
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
expect(screen.queryByText('Report')).not.toBeInTheDocument()
|
|
321
|
+
})
|
|
322
|
+
|
|
281
323
|
it('returns null when participant is undefined', () => {
|
|
282
324
|
const { container } = renderWithProviders(
|
|
283
325
|
<ChannelInfoDialog
|
|
@@ -33,6 +33,16 @@ export interface ChannelInfoDialogProps {
|
|
|
33
33
|
onLeaveConversation?: (channel: ChannelType) => void
|
|
34
34
|
onBlockParticipant?: (participantId?: string) => void
|
|
35
35
|
showDeleteConversation?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Show the Block/Unblock action. Defaults to true.
|
|
38
|
+
* Set false to hide it (e.g. the Linktree official channel).
|
|
39
|
+
*/
|
|
40
|
+
showBlockParticipant?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Show the Report action. Defaults to true.
|
|
43
|
+
* Set false to hide it (e.g. the Linktree official channel).
|
|
44
|
+
*/
|
|
45
|
+
showReportParticipant?: boolean
|
|
36
46
|
onDeleteConversationClick?: () => void
|
|
37
47
|
onBlockParticipantClick?: () => void
|
|
38
48
|
onReportParticipantClick?: () => void
|
|
@@ -53,6 +63,8 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
53
63
|
onLeaveConversation,
|
|
54
64
|
onBlockParticipant,
|
|
55
65
|
showDeleteConversation = true,
|
|
66
|
+
showBlockParticipant = true,
|
|
67
|
+
showReportParticipant = true,
|
|
56
68
|
onDeleteConversationClick,
|
|
57
69
|
onBlockParticipantClick,
|
|
58
70
|
onReportParticipantClick,
|
|
@@ -64,9 +76,11 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
64
76
|
const [isLeaving, setIsLeaving] = useState(false)
|
|
65
77
|
const [isUpdatingBlockStatus, setIsUpdatingBlockStatus] = useState(false)
|
|
66
78
|
|
|
67
|
-
// Check if participant is blocked when participant changes
|
|
79
|
+
// Check if participant is blocked when participant changes.
|
|
80
|
+
// Skipped when the Block action is hidden — the result would be unused
|
|
81
|
+
// (e.g. the Linktree official channel does not offer blocking).
|
|
68
82
|
const checkIsParticipantBlocked = useCallback(async () => {
|
|
69
|
-
if (!service || !participant?.user?.id) return
|
|
83
|
+
if (!showBlockParticipant || !service || !participant?.user?.id) return
|
|
70
84
|
|
|
71
85
|
try {
|
|
72
86
|
const blockedUsers = await service.getBlockedUsers()
|
|
@@ -80,7 +94,7 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
80
94
|
error
|
|
81
95
|
)
|
|
82
96
|
}
|
|
83
|
-
}, [service, participant?.user?.id])
|
|
97
|
+
}, [service, participant?.user?.id, showBlockParticipant])
|
|
84
98
|
|
|
85
99
|
useEffect(() => {
|
|
86
100
|
checkIsParticipantBlocked()
|
|
@@ -274,41 +288,45 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
274
288
|
</ActionButton>
|
|
275
289
|
</li>
|
|
276
290
|
)}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
291
|
+
{showBlockParticipant && (
|
|
292
|
+
<li>
|
|
293
|
+
{isParticipantBlocked ? (
|
|
294
|
+
<ActionButton
|
|
295
|
+
onClick={handleUnblockUser}
|
|
296
|
+
disabled={isUpdatingBlockStatus}
|
|
297
|
+
aria-busy={isUpdatingBlockStatus}
|
|
298
|
+
>
|
|
299
|
+
{isUpdatingBlockStatus ? (
|
|
300
|
+
<SpinnerGapIcon className="h-5 w-5 animate-spin" />
|
|
301
|
+
) : (
|
|
302
|
+
<ProhibitInsetIcon className="h-5 w-5" />
|
|
303
|
+
)}
|
|
304
|
+
<span>Unblock</span>
|
|
305
|
+
</ActionButton>
|
|
306
|
+
) : (
|
|
307
|
+
<ActionButton
|
|
308
|
+
onClick={handleBlockUser}
|
|
309
|
+
disabled={isUpdatingBlockStatus}
|
|
310
|
+
aria-busy={isUpdatingBlockStatus}
|
|
311
|
+
>
|
|
312
|
+
{isUpdatingBlockStatus ? (
|
|
313
|
+
<SpinnerGapIcon className="h-5 w-5 animate-spin" />
|
|
314
|
+
) : (
|
|
315
|
+
<ProhibitInsetIcon className="h-5 w-5" />
|
|
316
|
+
)}
|
|
317
|
+
<span>Block</span>
|
|
318
|
+
</ActionButton>
|
|
319
|
+
)}
|
|
320
|
+
</li>
|
|
321
|
+
)}
|
|
322
|
+
{showReportParticipant && (
|
|
323
|
+
<li>
|
|
324
|
+
<ActionButton variant="danger" onClick={handleReportUser}>
|
|
325
|
+
<FlagIcon className="h-5 w-5" />
|
|
326
|
+
<span>Report</span>
|
|
303
327
|
</ActionButton>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<li>
|
|
307
|
-
<ActionButton variant="danger" onClick={handleReportUser}>
|
|
308
|
-
<FlagIcon className="h-5 w-5" />
|
|
309
|
-
<span>Report</span>
|
|
310
|
-
</ActionButton>
|
|
311
|
-
</li>
|
|
328
|
+
</li>
|
|
329
|
+
)}
|
|
312
330
|
{customChannelActions}
|
|
313
331
|
</ul>
|
|
314
332
|
</div>
|
|
@@ -269,6 +269,31 @@ DmAgentHeader.parameters = {
|
|
|
269
269
|
},
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
export const RestrictedOfficialChannel: StoryFn<TemplateProps> = Template.bind(
|
|
273
|
+
{}
|
|
274
|
+
)
|
|
275
|
+
RestrictedOfficialChannel.args = {
|
|
276
|
+
showBackButton: false,
|
|
277
|
+
// Restricted surface for the Linktree official channel:
|
|
278
|
+
showBlockParticipant: false,
|
|
279
|
+
showReportParticipant: false,
|
|
280
|
+
showFollowerStatus: false,
|
|
281
|
+
composerDisabled: true,
|
|
282
|
+
composerDisabledReason:
|
|
283
|
+
'Message Linktree from your profile to start the conversation',
|
|
284
|
+
followerStatus: true, // would normally render "Subscribed to you" — suppressed here
|
|
285
|
+
onLeaveConversation: (channel) =>
|
|
286
|
+
console.log('Leave conversation:', channel.id),
|
|
287
|
+
}
|
|
288
|
+
RestrictedOfficialChannel.parameters = {
|
|
289
|
+
docs: {
|
|
290
|
+
description: {
|
|
291
|
+
story:
|
|
292
|
+
'Restricted action surface used by the Linktree official channel: block, report, and the subscription-status label are hidden, and the composer is locked with an explanatory reason. Delete conversation, favorite, and chat info remain available. Open the chat info dialog (3-dot / name click) to see block & report removed.',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
272
297
|
export const WithBackButton: StoryFn<TemplateProps> = Template.bind({})
|
|
273
298
|
WithBackButton.args = {
|
|
274
299
|
showBackButton: true,
|
|
@@ -45,12 +45,15 @@ vi.mock('../providers/MessagingProvider', () => ({
|
|
|
45
45
|
useMessagingContext: () => ({ service: null, debug: false }),
|
|
46
46
|
}))
|
|
47
47
|
|
|
48
|
+
const messageInputProps: Array<Record<string, unknown>> = []
|
|
48
49
|
vi.mock('./CustomMessageInput', () => ({
|
|
49
|
-
CustomMessageInput: ({
|
|
50
|
-
renderActions,
|
|
51
|
-
}: {
|
|
50
|
+
CustomMessageInput: (props: {
|
|
52
51
|
renderActions?: () => React.ReactNode
|
|
53
|
-
|
|
52
|
+
[key: string]: unknown
|
|
53
|
+
}) => {
|
|
54
|
+
messageInputProps.push({ ...props })
|
|
55
|
+
return <div data-testid="message-input">{props.renderActions?.()}</div>
|
|
56
|
+
},
|
|
54
57
|
}))
|
|
55
58
|
|
|
56
59
|
vi.mock('./CustomMessage', () => ({
|
|
@@ -65,8 +68,12 @@ vi.mock('./CustomDateSeparator', () => ({
|
|
|
65
68
|
CustomDateSeparator: () => <div data-testid="custom-date-separator" />,
|
|
66
69
|
}))
|
|
67
70
|
|
|
71
|
+
const channelInfoDialogProps: Array<Record<string, unknown>> = []
|
|
68
72
|
vi.mock('./ChannelInfoDialog', () => ({
|
|
69
|
-
ChannelInfoDialog: () =>
|
|
73
|
+
ChannelInfoDialog: (props: Record<string, unknown>) => {
|
|
74
|
+
channelInfoDialogProps.push({ ...props })
|
|
75
|
+
return <div data-testid="channel-info-dialog" />
|
|
76
|
+
},
|
|
70
77
|
}))
|
|
71
78
|
|
|
72
79
|
const avatarRenderCalls: Array<Record<string, unknown>> = []
|
|
@@ -144,9 +151,81 @@ describe('ChannelView', () => {
|
|
|
144
151
|
activeChannel = undefined
|
|
145
152
|
activeChannelProps = {}
|
|
146
153
|
avatarRenderCalls.length = 0
|
|
154
|
+
channelInfoDialogProps.length = 0
|
|
155
|
+
messageInputProps.length = 0
|
|
147
156
|
mockIsStarred = false
|
|
148
157
|
})
|
|
149
158
|
|
|
159
|
+
const lastDialogProps = () =>
|
|
160
|
+
channelInfoDialogProps[channelInfoDialogProps.length - 1]
|
|
161
|
+
const lastInputProps = () =>
|
|
162
|
+
messageInputProps[messageInputProps.length - 1]
|
|
163
|
+
|
|
164
|
+
it('keeps block and report visible by default in the info dialog', () => {
|
|
165
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
166
|
+
|
|
167
|
+
expect(lastDialogProps().showBlockParticipant).not.toBe(false)
|
|
168
|
+
expect(lastDialogProps().showReportParticipant).not.toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('hides block and report in the info dialog when restricted', () => {
|
|
172
|
+
renderWithProviders(
|
|
173
|
+
<ChannelView
|
|
174
|
+
channel={createChannel()}
|
|
175
|
+
showBlockParticipant={false}
|
|
176
|
+
showReportParticipant={false}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(lastDialogProps().showBlockParticipant).toBe(false)
|
|
181
|
+
expect(lastDialogProps().showReportParticipant).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('passes the follower status label to the info dialog by default', () => {
|
|
185
|
+
const channel = createChannel()
|
|
186
|
+
;(channel as unknown as { data: Record<string, unknown> }).data = {
|
|
187
|
+
isFollower: true,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
renderWithProviders(<ChannelView channel={channel} />)
|
|
191
|
+
|
|
192
|
+
expect(lastDialogProps().followerStatusLabel).toBe('Subscribed to you')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('suppresses the follower status label when showFollowerStatus is false', () => {
|
|
196
|
+
const channel = createChannel()
|
|
197
|
+
;(channel as unknown as { data: Record<string, unknown> }).data = {
|
|
198
|
+
isFollower: true,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
renderWithProviders(
|
|
202
|
+
<ChannelView channel={channel} showFollowerStatus={false} />
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
expect(lastDialogProps().followerStatusLabel).toBeUndefined()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('passes composer disabled state and reason to the message input', () => {
|
|
209
|
+
renderWithProviders(
|
|
210
|
+
<ChannelView
|
|
211
|
+
channel={createChannel()}
|
|
212
|
+
composerDisabled
|
|
213
|
+
composerDisabledReason="Message Linktree from your profile to reply"
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
expect(lastInputProps().disabled).toBe(true)
|
|
218
|
+
expect(lastInputProps().disabledReason).toBe(
|
|
219
|
+
'Message Linktree from your profile to reply'
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('does not disable the composer by default', () => {
|
|
224
|
+
renderWithProviders(<ChannelView channel={createChannel()} />)
|
|
225
|
+
|
|
226
|
+
expect(lastInputProps().disabled).not.toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
150
229
|
it('renders conversation footer between message list and message input', () => {
|
|
151
230
|
const channel = createChannel()
|
|
152
231
|
const renderConversationFooter = vi.fn((currentChannel: Channel) => (
|
|
@@ -259,6 +259,11 @@ const ChannelViewInner: React.FC<{
|
|
|
259
259
|
onDeleteConversationClick?: () => void
|
|
260
260
|
onBlockParticipantClick?: () => void
|
|
261
261
|
onReportParticipantClick?: () => void
|
|
262
|
+
showBlockParticipant?: boolean
|
|
263
|
+
showReportParticipant?: boolean
|
|
264
|
+
showFollowerStatus?: boolean
|
|
265
|
+
composerDisabled?: boolean
|
|
266
|
+
composerDisabledReason?: string
|
|
262
267
|
showStarButton?: boolean
|
|
263
268
|
chatbotVotingEnabled?: boolean
|
|
264
269
|
renderChannelBanner?: () => React.ReactNode
|
|
@@ -282,6 +287,11 @@ const ChannelViewInner: React.FC<{
|
|
|
282
287
|
onDeleteConversationClick,
|
|
283
288
|
onBlockParticipantClick,
|
|
284
289
|
onReportParticipantClick,
|
|
290
|
+
showBlockParticipant = true,
|
|
291
|
+
showReportParticipant = true,
|
|
292
|
+
showFollowerStatus = true,
|
|
293
|
+
composerDisabled = false,
|
|
294
|
+
composerDisabledReason,
|
|
285
295
|
showStarButton = false,
|
|
286
296
|
chatbotVotingEnabled = false,
|
|
287
297
|
renderChannelBanner,
|
|
@@ -323,8 +333,12 @@ const ChannelViewInner: React.FC<{
|
|
|
323
333
|
currentUserIsAccount === false &&
|
|
324
334
|
participantIsAccount === true
|
|
325
335
|
|
|
326
|
-
// Get follower status label from channel data
|
|
336
|
+
// Get follower status label from channel data.
|
|
337
|
+
// Suppressed entirely when showFollowerStatus is false (e.g. the Linktree
|
|
338
|
+
// official channel hides subscription status).
|
|
327
339
|
const followerStatusLabel = React.useMemo(() => {
|
|
340
|
+
if (!showFollowerStatus) return undefined
|
|
341
|
+
|
|
328
342
|
const channelExtraData = (channel.data ?? {}) as {
|
|
329
343
|
followerStatus?: string
|
|
330
344
|
isFollower?: boolean
|
|
@@ -342,7 +356,7 @@ const ChannelViewInner: React.FC<{
|
|
|
342
356
|
}
|
|
343
357
|
// Otherwise, don't show any status
|
|
344
358
|
return undefined
|
|
345
|
-
}, [channel.data])
|
|
359
|
+
}, [channel.data, showFollowerStatus])
|
|
346
360
|
|
|
347
361
|
const handleShowInfo = useCallback(() => {
|
|
348
362
|
infoDialogRef.current?.showModal()
|
|
@@ -424,6 +438,8 @@ const ChannelViewInner: React.FC<{
|
|
|
424
438
|
key="lt-channel-message-input"
|
|
425
439
|
renderActions={() => renderMessageInputActions?.(channel)}
|
|
426
440
|
renderFooter={() => renderMessageInputFooter?.(channel)}
|
|
441
|
+
disabled={composerDisabled}
|
|
442
|
+
disabledReason={composerDisabledReason}
|
|
427
443
|
/>
|
|
428
444
|
</Window>
|
|
429
445
|
</WithComponents>
|
|
@@ -439,6 +455,8 @@ const ChannelViewInner: React.FC<{
|
|
|
439
455
|
onLeaveConversation={onLeaveConversation}
|
|
440
456
|
onBlockParticipant={onBlockParticipant}
|
|
441
457
|
showDeleteConversation={showDeleteConversation}
|
|
458
|
+
showBlockParticipant={showBlockParticipant}
|
|
459
|
+
showReportParticipant={showReportParticipant}
|
|
442
460
|
onDeleteConversationClick={onDeleteConversationClick}
|
|
443
461
|
onBlockParticipantClick={onBlockParticipantClick}
|
|
444
462
|
onReportParticipantClick={onReportParticipantClick}
|
|
@@ -468,6 +486,11 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
468
486
|
onDeleteConversationClick,
|
|
469
487
|
onBlockParticipantClick,
|
|
470
488
|
onReportParticipantClick,
|
|
489
|
+
showBlockParticipant = true,
|
|
490
|
+
showReportParticipant = true,
|
|
491
|
+
showFollowerStatus = true,
|
|
492
|
+
composerDisabled = false,
|
|
493
|
+
composerDisabledReason,
|
|
471
494
|
dmAgentEnabled,
|
|
472
495
|
messageMetadata,
|
|
473
496
|
onMessageSent,
|
|
@@ -580,6 +603,11 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
580
603
|
onDeleteConversationClick={onDeleteConversationClick}
|
|
581
604
|
onBlockParticipantClick={onBlockParticipantClick}
|
|
582
605
|
onReportParticipantClick={onReportParticipantClick}
|
|
606
|
+
showBlockParticipant={showBlockParticipant}
|
|
607
|
+
showReportParticipant={showReportParticipant}
|
|
608
|
+
showFollowerStatus={showFollowerStatus}
|
|
609
|
+
composerDisabled={composerDisabled}
|
|
610
|
+
composerDisabledReason={composerDisabledReason}
|
|
583
611
|
showStarButton={showStarButton}
|
|
584
612
|
dmAgentEnabled={dmAgentEnabled}
|
|
585
613
|
chatbotVotingEnabled={chatbotVotingEnabled}
|
|
@@ -177,4 +177,46 @@ describe('CustomMessageInput', () => {
|
|
|
177
177
|
// useMessageComposerHasSendableData is mocked to return false
|
|
178
178
|
expect(sendButton).toBeDisabled()
|
|
179
179
|
})
|
|
180
|
+
|
|
181
|
+
it('locks the composer when the disabled prop is true (channel not frozen)', () => {
|
|
182
|
+
mockChannelData = {}
|
|
183
|
+
|
|
184
|
+
const { container } = renderWithProviders(<CustomMessageInput disabled />)
|
|
185
|
+
|
|
186
|
+
const messageInput = container.querySelector('.message-input')
|
|
187
|
+
expect(messageInput).toHaveAttribute('aria-disabled', 'true')
|
|
188
|
+
expect(messageInput).toHaveAttribute('inert')
|
|
189
|
+
|
|
190
|
+
const textarea = screen.getByTestId('textarea-composer')
|
|
191
|
+
expect(textarea).toHaveAttribute('readonly')
|
|
192
|
+
expect(textarea).toHaveAttribute('tabindex', '-1')
|
|
193
|
+
|
|
194
|
+
const sendButton = screen.getByRole('button', { name: /send/i })
|
|
195
|
+
expect(sendButton).toBeDisabled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('renders the disabled reason when the composer is disabled with a reason', () => {
|
|
199
|
+
mockChannelData = {}
|
|
200
|
+
|
|
201
|
+
renderWithProviders(
|
|
202
|
+
<CustomMessageInput
|
|
203
|
+
disabled
|
|
204
|
+
disabledReason="Message Linktree from your profile to reply"
|
|
205
|
+
/>
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
screen.getByText(/Message Linktree from your profile to reply/i)
|
|
210
|
+
).toBeInTheDocument()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('does not render the disabled reason when the composer is not disabled', () => {
|
|
214
|
+
mockChannelData = {}
|
|
215
|
+
|
|
216
|
+
renderWithProviders(
|
|
217
|
+
<CustomMessageInput disabledReason="should not show" />
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(screen.queryByText(/should not show/i)).not.toBeInTheDocument()
|
|
221
|
+
})
|
|
180
222
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArrowUpIcon } from '@phosphor-icons/react'
|
|
2
|
-
import React from 'react'
|
|
2
|
+
import React, { useCallback } from 'react'
|
|
3
3
|
import {
|
|
4
4
|
AttachmentPreviewList as DefaultAttachmentPreviewList,
|
|
5
5
|
MessageInput,
|
|
@@ -29,13 +29,13 @@ const DefaultSendButton: React.FC<{
|
|
|
29
29
|
</button>
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
const CustomMessageInputInner: React.FC = (
|
|
33
|
-
|
|
32
|
+
const CustomMessageInputInner: React.FC<{ disabled?: boolean }> = ({
|
|
33
|
+
disabled = false,
|
|
34
|
+
}) => {
|
|
34
35
|
const { handleSubmit } = useMessageInputContext()
|
|
35
36
|
|
|
36
37
|
const hasSendableData = useMessageComposerHasSendableData()
|
|
37
|
-
const
|
|
38
|
-
const isSendDisabled = isFrozen || !hasSendableData
|
|
38
|
+
const isSendDisabled = disabled || !hasSendableData
|
|
39
39
|
|
|
40
40
|
const {
|
|
41
41
|
SendButton = DefaultSendButton,
|
|
@@ -50,16 +50,16 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
50
50
|
<div className="flex">
|
|
51
51
|
<div className="w-full ml-2 mr-4 self-center leading-[0]">
|
|
52
52
|
<TextareaComposer
|
|
53
|
-
aria-disabled={
|
|
53
|
+
aria-disabled={disabled || undefined}
|
|
54
54
|
className="w-full resize-none outline-none leading-6"
|
|
55
55
|
// While this might usually be considered an anti-pattern, in most
|
|
56
56
|
// cases, when a message thread is rendered, we want the input to
|
|
57
57
|
// gain focus automatically.
|
|
58
58
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
59
|
-
autoFocus={!
|
|
59
|
+
autoFocus={!disabled}
|
|
60
60
|
maxRows={4}
|
|
61
|
-
readOnly={
|
|
62
|
-
tabIndex={
|
|
61
|
+
readOnly={disabled}
|
|
62
|
+
tabIndex={disabled ? -1 : undefined}
|
|
63
63
|
/>
|
|
64
64
|
</div>
|
|
65
65
|
<SendButton
|
|
@@ -78,21 +78,42 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
78
78
|
export interface CustomMessageInputProps {
|
|
79
79
|
renderActions?: () => React.ReactNode
|
|
80
80
|
renderFooter?: () => React.ReactNode
|
|
81
|
+
/**
|
|
82
|
+
* Lock the composer (read-only textarea, disabled send, no autofocus).
|
|
83
|
+
* Combined with the channel's `frozen` flag — either one locks the input.
|
|
84
|
+
* Used by the Linktree official channel, where the composer stays locked
|
|
85
|
+
* until the linker messages Linktree from its public profile.
|
|
86
|
+
* Defaults to false.
|
|
87
|
+
*/
|
|
88
|
+
disabled?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* Explanatory text rendered below the composer while it is locked.
|
|
91
|
+
* Only shown when the composer is disabled (via `disabled` or `frozen`).
|
|
92
|
+
*/
|
|
93
|
+
disabledReason?: string
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
|
|
84
97
|
renderActions,
|
|
85
98
|
renderFooter,
|
|
99
|
+
disabled = false,
|
|
100
|
+
disabledReason,
|
|
86
101
|
}) => {
|
|
87
102
|
const { channel } = useChannelStateContext()
|
|
88
103
|
const isFrozen = channel?.data?.frozen === true
|
|
104
|
+
const isLocked = isFrozen || disabled
|
|
105
|
+
|
|
106
|
+
const Input = useCallback(
|
|
107
|
+
() => <CustomMessageInputInner disabled={isLocked} />,
|
|
108
|
+
[isLocked]
|
|
109
|
+
)
|
|
89
110
|
|
|
90
111
|
return (
|
|
91
112
|
<div className="flex flex-col gap-4 p-4">
|
|
92
113
|
<div
|
|
93
114
|
// @ts-expect-error Only React 19 onwards has `inert` in its types.
|
|
94
|
-
inert={
|
|
95
|
-
aria-disabled={
|
|
115
|
+
inert={isLocked ? '' : undefined}
|
|
116
|
+
aria-disabled={isLocked || undefined}
|
|
96
117
|
className="message-input flex items-end gap-4 aria-disabled:opacity-40"
|
|
97
118
|
>
|
|
98
119
|
{renderActions && (
|
|
@@ -100,8 +121,13 @@ export const CustomMessageInput: React.FC<CustomMessageInputProps> = ({
|
|
|
100
121
|
{renderActions()}
|
|
101
122
|
</div>
|
|
102
123
|
)}
|
|
103
|
-
<MessageInput Input={
|
|
124
|
+
<MessageInput Input={Input} />
|
|
104
125
|
</div>
|
|
126
|
+
{isLocked && disabledReason ? (
|
|
127
|
+
<p className="message-input-disabled-reason px-2 text-center text-sm text-black/55">
|
|
128
|
+
{disabledReason}
|
|
129
|
+
</p>
|
|
130
|
+
) : null}
|
|
105
131
|
{renderFooter?.()}
|
|
106
132
|
</div>
|
|
107
133
|
)
|