@linktr.ee/messaging-react 3.0.0 → 3.1.0
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-C-ZIQW_q.js → Card-BGOWR4lW.js} +2 -2
- package/dist/{Card-C-ZIQW_q.js.map → Card-BGOWR4lW.js.map} +1 -1
- package/dist/{Card-Cqld0-Ws.js → Card-BfvsO78k.js} +3 -3
- package/dist/{Card-Cqld0-Ws.js.map → Card-BfvsO78k.js.map} +1 -1
- package/dist/{Card-C46z9zz4.js → Card-BhO5jeP9.js} +2 -2
- package/dist/{Card-C46z9zz4.js.map → Card-BhO5jeP9.js.map} +1 -1
- package/dist/{Card-Cq0x0bbb.cjs → Card-CRJ4l5KM.cjs} +2 -2
- package/dist/{Card-Cq0x0bbb.cjs.map → Card-CRJ4l5KM.cjs.map} +1 -1
- package/dist/{Card-Drz28Q-Y.cjs → Card-Cq-cN9n1.cjs} +2 -2
- package/dist/{Card-Drz28Q-Y.cjs.map → Card-Cq-cN9n1.cjs.map} +1 -1
- package/dist/{Card-B7ePjYQ6.cjs → Card-NPXVehHb.cjs} +2 -2
- package/dist/{Card-B7ePjYQ6.cjs.map → Card-NPXVehHb.cjs.map} +1 -1
- package/dist/{LockedThumbnail--h4GTH41.cjs → LockedThumbnail-B8MKBVXz.cjs} +2 -2
- package/dist/{LockedThumbnail--h4GTH41.cjs.map → LockedThumbnail-B8MKBVXz.cjs.map} +1 -1
- package/dist/{LockedThumbnail-D5NHhET2.js → LockedThumbnail-Bu9jNPUi.js} +2 -2
- package/dist/{LockedThumbnail-D5NHhET2.js.map → LockedThumbnail-Bu9jNPUi.js.map} +1 -1
- package/dist/{index-BUT2yBvJ.js → index-CJEl_fID.js} +1417 -1391
- package/dist/index-CJEl_fID.js.map +1 -0
- package/dist/index-D-5Igybf.cjs +2 -0
- package/dist/index-D-5Igybf.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +33 -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 +27 -3
- package/src/components/ChannelView.test.tsx +84 -5
- package/src/components/ChannelView.tsx +30 -2
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +40 -0
- package/src/components/CustomMessageInput/index.tsx +53 -9
- package/src/types.ts +38 -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-D-5Igybf.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,39 @@ 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
|
+
* Replace the message composer with a non-interactive locked panel showing
|
|
204
|
+
* `composerDisabledReason`. Defaults to false. Used by the Linktree official
|
|
205
|
+
* channel, where the linker cannot message Linktree from the inbox (they
|
|
206
|
+
* message Linktree from its public profile instead).
|
|
207
|
+
*
|
|
208
|
+
* Distinct from the channel's `frozen` flag, which keeps the composer
|
|
209
|
+
* rendered but read-only/dimmed.
|
|
210
|
+
*/
|
|
211
|
+
composerDisabled?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Explanatory text shown inside the locked panel. Only rendered when
|
|
214
|
+
* `composerDisabled` is true.
|
|
215
|
+
*/
|
|
216
|
+
composerDisabledReason?: string;
|
|
184
217
|
/**
|
|
185
218
|
* When true and DM agent is active on the channel (not paused),
|
|
186
219
|
* 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-CJEl_fID.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>
|
|
@@ -225,7 +225,7 @@ const Template: StoryFn<TemplateProps> = (args) => {
|
|
|
225
225
|
|
|
226
226
|
return (
|
|
227
227
|
<Chat client={client}>
|
|
228
|
-
<div className="h-screen w-full bg-
|
|
228
|
+
<div className="h-screen w-full bg-[#FBFAF9]">
|
|
229
229
|
<ChannelView {...channelViewProps} channel={channel} />
|
|
230
230
|
</div>
|
|
231
231
|
</Chat>
|
|
@@ -269,6 +269,30 @@ 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: 'Only Linktree can send messages on this thread',
|
|
283
|
+
followerStatus: true, // would normally render "Subscribed to you" — suppressed here
|
|
284
|
+
onLeaveConversation: (channel) =>
|
|
285
|
+
console.log('Leave conversation:', channel.id),
|
|
286
|
+
}
|
|
287
|
+
RestrictedOfficialChannel.parameters = {
|
|
288
|
+
docs: {
|
|
289
|
+
description: {
|
|
290
|
+
story:
|
|
291
|
+
'Restricted action surface used by the Linktree official channel: block, report, and the subscription-status label are hidden, and the composer is replaced by a locked panel explaining the linker cannot send messages on this thread. Delete conversation, favorite, and chat info remain available. Open the chat info dialog (3-dot / name click) to see block & report removed.',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
|
|
272
296
|
export const WithBackButton: StoryFn<TemplateProps> = Template.bind({})
|
|
273
297
|
WithBackButton.args = {
|
|
274
298
|
showBackButton: true,
|
|
@@ -438,7 +462,7 @@ const WithStarButtonTemplate: StoryFn<ComponentProps> = (args) => {
|
|
|
438
462
|
|
|
439
463
|
return (
|
|
440
464
|
<Chat client={client}>
|
|
441
|
-
<div className="h-screen w-full bg-
|
|
465
|
+
<div className="h-screen w-full bg-[#FBFAF9]">
|
|
442
466
|
<ChannelView {...args} channel={channel} />
|
|
443
467
|
</div>
|
|
444
468
|
</Chat>
|
|
@@ -495,7 +519,7 @@ const EmptyTemplate: StoryFn<ComponentProps> = (args) => {
|
|
|
495
519
|
|
|
496
520
|
return (
|
|
497
521
|
<Chat client={client}>
|
|
498
|
-
<div className="h-screen w-full bg-
|
|
522
|
+
<div className="h-screen w-full bg-[#FBFAF9]">
|
|
499
523
|
<ChannelView {...args} channel={channel} />
|
|
500
524
|
</div>
|
|
501
525
|
</Chat>
|
|
@@ -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,44 @@ describe('CustomMessageInput', () => {
|
|
|
177
177
|
// useMessageComposerHasSendableData is mocked to return false
|
|
178
178
|
expect(sendButton).toBeDisabled()
|
|
179
179
|
})
|
|
180
|
+
|
|
181
|
+
it('replaces the composer with the locked panel when disabled (channel not frozen)', () => {
|
|
182
|
+
mockChannelData = {}
|
|
183
|
+
|
|
184
|
+
const { container } = renderWithProviders(
|
|
185
|
+
<CustomMessageInput
|
|
186
|
+
disabled
|
|
187
|
+
disabledReason="Only Linktree can send messages on this thread"
|
|
188
|
+
/>
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// The interactive input is gone entirely — no textarea, no send button.
|
|
192
|
+
expect(
|
|
193
|
+
screen.queryByTestId('stream-message-input')
|
|
194
|
+
).not.toBeInTheDocument()
|
|
195
|
+
expect(screen.queryByTestId('textarea-composer')).not.toBeInTheDocument()
|
|
196
|
+
expect(container.querySelector('.message-input')).not.toBeInTheDocument()
|
|
197
|
+
|
|
198
|
+
// The locked panel with the reason replaces it.
|
|
199
|
+
expect(
|
|
200
|
+
container.querySelector('.messaging-composer-locked-panel')
|
|
201
|
+
).toBeInTheDocument()
|
|
202
|
+
expect(
|
|
203
|
+
screen.getByText(/Only Linktree can send messages on this thread/i)
|
|
204
|
+
).toBeInTheDocument()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('does not render the locked panel when the composer is not disabled', () => {
|
|
208
|
+
mockChannelData = {}
|
|
209
|
+
|
|
210
|
+
const { container } = renderWithProviders(
|
|
211
|
+
<CustomMessageInput disabledReason="should not show" />
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
expect(
|
|
215
|
+
container.querySelector('.messaging-composer-locked-panel')
|
|
216
|
+
).not.toBeInTheDocument()
|
|
217
|
+
expect(screen.queryByText(/should not show/i)).not.toBeInTheDocument()
|
|
218
|
+
expect(screen.getByTestId('stream-message-input')).toBeInTheDocument()
|
|
219
|
+
})
|
|
180
220
|
})
|