@linktr.ee/messaging-react 2.2.0 → 2.2.2-rc-1779314025
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-EKxCn56j.js → Card-CAliTKdt.js} +3 -3
- package/dist/{Card-EKxCn56j.js.map → Card-CAliTKdt.js.map} +1 -1
- package/dist/Card-CRMpOj0f.cjs +2 -0
- package/dist/Card-CRMpOj0f.cjs.map +1 -0
- package/dist/{Card-ChR37pLZ.js → Card-Ca5PnHml.js} +2 -2
- package/dist/{Card-ChR37pLZ.js.map → Card-Ca5PnHml.js.map} +1 -1
- package/dist/{Card-BdTueeyk.js → Card-Dz2v3fXf.js} +2 -2
- package/dist/{Card-BdTueeyk.js.map → Card-Dz2v3fXf.js.map} +1 -1
- package/dist/Card-b41LWND_.cjs +2 -0
- package/dist/Card-b41LWND_.cjs.map +1 -0
- package/dist/Card-vEkarkVD.cjs +2 -0
- package/dist/Card-vEkarkVD.cjs.map +1 -0
- package/dist/{LockedThumbnail-B16qP3eH.js → LockedThumbnail-BGz0NIQh.js} +2 -2
- package/dist/{LockedThumbnail-B16qP3eH.js.map → LockedThumbnail-BGz0NIQh.js.map} +1 -1
- package/dist/LockedThumbnail-JuPkpHeX.cjs +2 -0
- package/dist/LockedThumbnail-JuPkpHeX.cjs.map +1 -0
- package/dist/index-CctUDJSJ.cjs +2 -0
- package/dist/index-CctUDJSJ.cjs.map +1 -0
- package/dist/{index-Dn7BC9xK.js → index-D55UTfgC.js} +1307 -1264
- package/dist/index-D55UTfgC.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -16
- package/package.json +4 -3
- package/src/components/ChannelInfoDialog/ChannelInfoDialog.test.tsx +44 -0
- package/src/components/ChannelInfoDialog/index.tsx +5 -1
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +35 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +2 -1
- package/src/components/ChannelView.test.tsx +65 -3
- package/src/components/ChannelView.tsx +53 -16
- package/src/index.ts +5 -0
- package/src/types.ts +9 -0
- package/src/utils/resolveParticipantDisplayName.test.ts +73 -0
- package/src/utils/resolveParticipantDisplayName.ts +40 -0
- package/dist/index-Dn7BC9xK.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-CctUDJSJ.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
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
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 { ChannelListProps as ChannelListProps_2 } from 'stream-chat-react';
|
|
5
|
+
import { ChannelMemberResponse } from 'stream-chat';
|
|
5
6
|
import { ChannelSort } from 'stream-chat';
|
|
6
7
|
import { ComponentType } from 'react';
|
|
7
8
|
import { default as default_2 } from 'react';
|
|
@@ -239,6 +240,11 @@ export declare interface ChannelViewProps {
|
|
|
239
240
|
* Falls back to message.text when no matching translation exists.
|
|
240
241
|
*/
|
|
241
242
|
viewerLanguage?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Resolves the display label for the other channel participant.
|
|
245
|
+
* Defaults to name, then username, then "Unknown member" (never user.id).
|
|
246
|
+
*/
|
|
247
|
+
getParticipantDisplayName?: (participant: ChannelMemberResponse | undefined) => string;
|
|
242
248
|
/**
|
|
243
249
|
* Custom render function for a banner/card component that renders
|
|
244
250
|
* between the channel header and message list.
|
|
@@ -458,6 +464,8 @@ declare type ImageLoadingMode = 'lazy' | 'eager';
|
|
|
458
464
|
|
|
459
465
|
export declare function isLinkAttachment(a: Attachment): boolean;
|
|
460
466
|
|
|
467
|
+
export declare function isUuidLike(value: string): boolean;
|
|
468
|
+
|
|
461
469
|
/**
|
|
462
470
|
* Link previews (1P / 3P Link Apps) shown in the chat thread. Mirrors
|
|
463
471
|
* the `LockedAttachment` API — render `LinkAttachment.Composer` while
|
|
@@ -1167,6 +1175,12 @@ export declare interface Participant {
|
|
|
1167
1175
|
metadata?: Record<string, unknown>;
|
|
1168
1176
|
}
|
|
1169
1177
|
|
|
1178
|
+
export declare type ParticipantDisplayUser = {
|
|
1179
|
+
id?: string;
|
|
1180
|
+
name?: string | null;
|
|
1181
|
+
username?: string | null;
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1170
1184
|
/**
|
|
1171
1185
|
* Message metadata for paid messaging and chatbot flows.
|
|
1172
1186
|
* Used to identify message types and payment status.
|
|
@@ -1232,6 +1246,12 @@ export declare function resolveLinkAttachment(message: LocalMessage): Attachment
|
|
|
1232
1246
|
|
|
1233
1247
|
export declare function resolveMediaFromMessage(message: LocalMessage): MediaMessageResolved | null;
|
|
1234
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* Resolves a human-readable participant label from Stream user fields.
|
|
1251
|
+
* Never falls back to user.id. UUID-like names are treated as missing.
|
|
1252
|
+
*/
|
|
1253
|
+
export declare function resolveParticipantDisplayName(user?: ParticipantDisplayUser | null): string;
|
|
1254
|
+
|
|
1235
1255
|
export declare interface SentCardProps extends LockedAttachmentBaseProps {
|
|
1236
1256
|
/** Placeholder shown in the title slot when no title is set. */
|
|
1237
1257
|
placeholderTitle?: string;
|
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
|
|
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-D55UTfgC.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
|
@@ -10,23 +10,25 @@ export {
|
|
|
10
10
|
l as FaqListItem,
|
|
11
11
|
r as LinkAttachment,
|
|
12
12
|
M as LockedAttachment,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
u as MediaMessage,
|
|
14
|
+
L as MessageAttachment,
|
|
15
|
+
c as MessageVoteButtons,
|
|
16
|
+
h as MessagingProvider,
|
|
17
17
|
d as MessagingShell,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
p as buildCompactMetaLabel,
|
|
19
|
+
v as formatFileSize,
|
|
20
|
+
A as formatRelativeTime,
|
|
21
|
+
C as getFileExtensionLabel,
|
|
22
|
+
F as getMessageDisplayText,
|
|
23
23
|
k as isLinkAttachment,
|
|
24
|
-
b as
|
|
25
|
-
f as
|
|
26
|
-
x as
|
|
27
|
-
|
|
24
|
+
b as isUuidLike,
|
|
25
|
+
f as messageAttachmentGroupPositionFromStream,
|
|
26
|
+
x as normalizeLanguageCode,
|
|
27
|
+
y as resolveLinkAttachment,
|
|
28
|
+
P as resolveMediaFromMessage,
|
|
29
|
+
S as resolveParticipantDisplayName,
|
|
28
30
|
q as useCustomMessage,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
z as useMessageVote,
|
|
32
|
+
B as useMessaging
|
|
31
33
|
};
|
|
32
34
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2-rc-1779314025",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js"
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
13
14
|
},
|
|
14
15
|
"./styles.css": "./dist/assets/index.css"
|
|
15
16
|
},
|
|
@@ -98,6 +98,50 @@ describe('ChannelInfoDialog', () => {
|
|
|
98
98
|
expect(nameEl).toBeInTheDocument()
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
it('uses participantDisplayName when provided', () => {
|
|
102
|
+
renderWithProviders(
|
|
103
|
+
<ChannelInfoDialog
|
|
104
|
+
{...defaultProps()}
|
|
105
|
+
participantDisplayName="Custom Label"
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(screen.getByText('Custom Label', { selector: 'p' })).toBeInTheDocument()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('falls back to username when name is UUID-like', () => {
|
|
113
|
+
renderWithProviders(
|
|
114
|
+
<ChannelInfoDialog
|
|
115
|
+
{...defaultProps()}
|
|
116
|
+
participant={createParticipant({
|
|
117
|
+
name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
|
|
118
|
+
username: 'linkeruser',
|
|
119
|
+
})}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('linkeruser', { selector: 'p' })).toBeInTheDocument()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('shows Unknown member when name matches user id', () => {
|
|
127
|
+
renderWithProviders(
|
|
128
|
+
<ChannelInfoDialog
|
|
129
|
+
{...defaultProps()}
|
|
130
|
+
participant={{
|
|
131
|
+
user: {
|
|
132
|
+
id: 'opaque-user-id',
|
|
133
|
+
name: 'opaque-user-id',
|
|
134
|
+
},
|
|
135
|
+
role: 'member',
|
|
136
|
+
} as unknown as ChannelMemberResponse}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
screen.getByText('Unknown member', { selector: 'p' })
|
|
142
|
+
).toBeInTheDocument()
|
|
143
|
+
})
|
|
144
|
+
|
|
101
145
|
it('renders participant username as secondary info', () => {
|
|
102
146
|
renderWithProviders(
|
|
103
147
|
<ChannelInfoDialog
|
|
@@ -8,6 +8,7 @@ import React, { useState, useCallback, useEffect } from 'react'
|
|
|
8
8
|
import type { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat'
|
|
9
9
|
|
|
10
10
|
import { useMessagingContext } from '../../providers/MessagingProvider'
|
|
11
|
+
import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
|
|
11
12
|
import ActionButton from '../ActionButton'
|
|
12
13
|
import { Avatar } from '../Avatar'
|
|
13
14
|
import { CloseButton } from '../CloseButton'
|
|
@@ -26,6 +27,7 @@ export interface ChannelInfoDialogProps {
|
|
|
26
27
|
dialogRef: React.RefObject<HTMLDialogElement>
|
|
27
28
|
onClose: () => void
|
|
28
29
|
participant: ChannelMemberResponse | undefined
|
|
30
|
+
participantDisplayName?: string
|
|
29
31
|
channel: ChannelType
|
|
30
32
|
followerStatusLabel?: string
|
|
31
33
|
onLeaveConversation?: (channel: ChannelType) => void
|
|
@@ -45,6 +47,7 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
45
47
|
dialogRef,
|
|
46
48
|
onClose,
|
|
47
49
|
participant,
|
|
50
|
+
participantDisplayName,
|
|
48
51
|
channel,
|
|
49
52
|
followerStatusLabel,
|
|
50
53
|
onLeaveConversation,
|
|
@@ -177,7 +180,8 @@ export const ChannelInfoDialog: React.FC<ChannelInfoDialogProps> = ({
|
|
|
177
180
|
if (!participant) return null
|
|
178
181
|
|
|
179
182
|
const participantName =
|
|
180
|
-
|
|
183
|
+
participantDisplayName ??
|
|
184
|
+
resolveParticipantDisplayName(participant.user)
|
|
181
185
|
const participantImage = participant.user?.image
|
|
182
186
|
const participantUsername = (participant.user as CustomUser)?.username
|
|
183
187
|
const participantSecondary = participantUsername
|
|
@@ -66,6 +66,41 @@ describe('CustomChannelPreview', () => {
|
|
|
66
66
|
onChannelSelect: vi.fn(),
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
it('uses username when participant name is UUID-like', () => {
|
|
70
|
+
const channel = createMockChannel([])
|
|
71
|
+
channel.state.members['participant-1'] = {
|
|
72
|
+
user: {
|
|
73
|
+
id: 'participant-1',
|
|
74
|
+
name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
|
|
75
|
+
username: 'alice',
|
|
76
|
+
},
|
|
77
|
+
user_id: 'participant-1',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
renderWithProviders(
|
|
81
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(screen.getByText('alice')).toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('shows Unknown member when participant has no usable name or username', () => {
|
|
88
|
+
const channel = createMockChannel([])
|
|
89
|
+
channel.state.members['participant-1'] = {
|
|
90
|
+
user: {
|
|
91
|
+
id: 'participant-1',
|
|
92
|
+
name: 'participant-1',
|
|
93
|
+
},
|
|
94
|
+
user_id: 'participant-1',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
renderWithProviders(
|
|
98
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText('Unknown member')).toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
69
104
|
it('shows the latest non-system message when the last message is a system message', () => {
|
|
70
105
|
const channel = createMockChannel([
|
|
71
106
|
{
|
|
@@ -5,6 +5,7 @@ import { ChannelPreviewUIComponentProps } from 'stream-chat-react'
|
|
|
5
5
|
import { useChannelStar } from '../../hooks/useChannelStar'
|
|
6
6
|
import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
7
7
|
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
|
|
8
|
+
import { resolveParticipantDisplayName } from '../../utils/resolveParticipantDisplayName'
|
|
8
9
|
import { Avatar } from '../Avatar'
|
|
9
10
|
import { isChatbotMessage } from '../CustomMessage/MessageTag'
|
|
10
11
|
|
|
@@ -45,7 +46,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
45
46
|
const participant = members.find(
|
|
46
47
|
(member) => member.user?.id && member.user.id !== channel?._client?.userID
|
|
47
48
|
)
|
|
48
|
-
const participantName = participant?.user
|
|
49
|
+
const participantName = resolveParticipantDisplayName(participant?.user)
|
|
49
50
|
const participantImage = participant?.user?.image
|
|
50
51
|
|
|
51
52
|
// Get last non-system message for preview
|
|
@@ -85,9 +85,21 @@ vi.mock('../hooks/useChannelStar', () => ({
|
|
|
85
85
|
const createChannel = ({
|
|
86
86
|
currentUserId = 'visitor-1',
|
|
87
87
|
currentUserRole = 'owner',
|
|
88
|
+
otherUser = {
|
|
89
|
+
id: 'linker-1' as const,
|
|
90
|
+
name: 'Linker' as string | undefined,
|
|
91
|
+
username: undefined as string | undefined,
|
|
92
|
+
is_account: true,
|
|
93
|
+
},
|
|
88
94
|
}: {
|
|
89
95
|
currentUserId?: 'visitor-1' | 'linker-1'
|
|
90
96
|
currentUserRole?: 'owner' | 'member'
|
|
97
|
+
otherUser?: {
|
|
98
|
+
id: string
|
|
99
|
+
name?: string
|
|
100
|
+
username?: string
|
|
101
|
+
is_account?: boolean
|
|
102
|
+
}
|
|
91
103
|
} = {}) =>
|
|
92
104
|
({
|
|
93
105
|
id: 'channel-1',
|
|
@@ -106,9 +118,12 @@ const createChannel = ({
|
|
|
106
118
|
},
|
|
107
119
|
other: {
|
|
108
120
|
user: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
...otherUser,
|
|
122
|
+
id:
|
|
123
|
+
otherUser.id ??
|
|
124
|
+
(currentUserId === 'visitor-1' ? 'linker-1' : 'visitor-1'),
|
|
125
|
+
is_account:
|
|
126
|
+
otherUser.is_account ?? currentUserId === 'visitor-1',
|
|
112
127
|
},
|
|
113
128
|
role: currentUserRole === 'owner' ? 'member' : 'owner',
|
|
114
129
|
},
|
|
@@ -294,4 +309,51 @@ describe('ChannelView', () => {
|
|
|
294
309
|
|
|
295
310
|
expect(activeChannelProps.SendButton).toBeUndefined()
|
|
296
311
|
})
|
|
312
|
+
|
|
313
|
+
it('uses username when participant name is UUID-like', () => {
|
|
314
|
+
renderWithProviders(
|
|
315
|
+
<ChannelView
|
|
316
|
+
channel={createChannel({
|
|
317
|
+
otherUser: {
|
|
318
|
+
id: 'linker-1',
|
|
319
|
+
name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
|
|
320
|
+
username: 'linkeruser',
|
|
321
|
+
is_account: true,
|
|
322
|
+
},
|
|
323
|
+
})}
|
|
324
|
+
/>
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
expect(screen.getAllByText('linkeruser').length).toBeGreaterThan(0)
|
|
328
|
+
expect(avatarRenderCalls.some((call) => call.name === 'linkeruser')).toBe(
|
|
329
|
+
true
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('shows Unknown member when participant has no usable name or username', () => {
|
|
334
|
+
renderWithProviders(
|
|
335
|
+
<ChannelView
|
|
336
|
+
channel={createChannel({
|
|
337
|
+
otherUser: {
|
|
338
|
+
id: 'opaque-user-id',
|
|
339
|
+
name: 'opaque-user-id',
|
|
340
|
+
is_account: true,
|
|
341
|
+
},
|
|
342
|
+
})}
|
|
343
|
+
/>
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
expect(screen.getAllByText('Unknown member').length).toBeGreaterThan(0)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('uses getParticipantDisplayName override when provided', () => {
|
|
350
|
+
renderWithProviders(
|
|
351
|
+
<ChannelView
|
|
352
|
+
channel={createChannel()}
|
|
353
|
+
getParticipantDisplayName={() => 'Custom Label'}
|
|
354
|
+
/>
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
expect(screen.getAllByText('Custom Label').length).toBeGreaterThan(0)
|
|
358
|
+
})
|
|
297
359
|
})
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from '@phosphor-icons/react'
|
|
8
8
|
import classNames from 'classnames'
|
|
9
9
|
import React, { useCallback, useRef } from 'react'
|
|
10
|
-
import { Channel as ChannelType } from 'stream-chat'
|
|
10
|
+
import { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat'
|
|
11
11
|
import {
|
|
12
12
|
Channel,
|
|
13
13
|
Window,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
|
|
21
21
|
import { useChannelStar } from '../hooks/useChannelStar'
|
|
22
22
|
import type { ChannelViewProps } from '../types'
|
|
23
|
+
import { resolveParticipantDisplayName } from '../utils/resolveParticipantDisplayName'
|
|
23
24
|
|
|
24
25
|
import { Avatar } from './Avatar'
|
|
25
26
|
import { ChannelInfoDialog } from './ChannelInfoDialog'
|
|
@@ -46,6 +47,9 @@ const CustomChannelHeader: React.FC<{
|
|
|
46
47
|
canShowInfo: boolean
|
|
47
48
|
showStarButton?: boolean
|
|
48
49
|
dmAgentEnabled?: boolean
|
|
50
|
+
getParticipantDisplayName: (
|
|
51
|
+
participant: ChannelMemberResponse | undefined
|
|
52
|
+
) => string
|
|
49
53
|
}> = ({
|
|
50
54
|
onBack,
|
|
51
55
|
showBackButton,
|
|
@@ -53,19 +57,21 @@ const CustomChannelHeader: React.FC<{
|
|
|
53
57
|
canShowInfo,
|
|
54
58
|
showStarButton = false,
|
|
55
59
|
dmAgentEnabled = false,
|
|
60
|
+
getParticipantDisplayName,
|
|
56
61
|
}) => {
|
|
57
62
|
const { channel } = useChannelStateContext()
|
|
58
63
|
|
|
59
64
|
// Get participant info (excluding current user)
|
|
60
65
|
const participant = React.useMemo(() => {
|
|
61
|
-
const
|
|
66
|
+
const myUserId = channel._client?.userID
|
|
67
|
+
if (!myUserId) return undefined
|
|
68
|
+
const members = Object.values(channel.state?.members || {})
|
|
62
69
|
return members.find(
|
|
63
|
-
(member) => member.user?.id && member.user.id !==
|
|
70
|
+
(member) => member.user?.id && member.user.id !== myUserId
|
|
64
71
|
)
|
|
65
|
-
}, [channel._client
|
|
72
|
+
}, [channel._client?.userID, channel.state?.members])
|
|
66
73
|
|
|
67
|
-
const participantName =
|
|
68
|
-
participant?.user?.name || participant?.user?.id || 'Unknown member'
|
|
74
|
+
const participantName = getParticipantDisplayName(participant)
|
|
69
75
|
const participantImage = participant?.user?.image
|
|
70
76
|
const isStarred = useChannelStar(channel)
|
|
71
77
|
|
|
@@ -259,6 +265,9 @@ const ChannelViewInner: React.FC<{
|
|
|
259
265
|
) => React.ReactNode
|
|
260
266
|
dmAgentEnabled?: boolean
|
|
261
267
|
viewerLanguage?: string
|
|
268
|
+
getParticipantDisplayName: (
|
|
269
|
+
participant: ChannelMemberResponse | undefined
|
|
270
|
+
) => string
|
|
262
271
|
}> = ({
|
|
263
272
|
onBack,
|
|
264
273
|
showBackButton,
|
|
@@ -278,22 +287,27 @@ const ChannelViewInner: React.FC<{
|
|
|
278
287
|
renderMessage,
|
|
279
288
|
dmAgentEnabled = false,
|
|
280
289
|
viewerLanguage,
|
|
290
|
+
getParticipantDisplayName,
|
|
281
291
|
}) => {
|
|
282
292
|
const { channel } = useChannelStateContext()
|
|
283
293
|
const infoDialogRef = useRef<HTMLDialogElement>(null)
|
|
284
294
|
|
|
285
295
|
// Get participant info for info dialog
|
|
286
296
|
const participant = React.useMemo(() => {
|
|
287
|
-
const
|
|
297
|
+
const myUserId = channel._client?.userID
|
|
298
|
+
if (!myUserId) return undefined
|
|
299
|
+
const members = Object.values(channel.state?.members || {})
|
|
288
300
|
return members.find(
|
|
289
|
-
(member) => member.user?.id && member.user.id !==
|
|
301
|
+
(member) => member.user?.id && member.user.id !== myUserId
|
|
290
302
|
)
|
|
291
|
-
}, [channel._client
|
|
303
|
+
}, [channel._client?.userID, channel.state?.members])
|
|
292
304
|
|
|
293
305
|
const currentMember = React.useMemo(() => {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
306
|
+
const myUserId = channel._client?.userID
|
|
307
|
+
if (!myUserId) return undefined
|
|
308
|
+
const members = Object.values(channel.state?.members || {})
|
|
309
|
+
return members.find((member) => member.user?.id === myUserId)
|
|
310
|
+
}, [channel._client?.userID, channel.state?.members])
|
|
297
311
|
|
|
298
312
|
const currentUserIsAccount =
|
|
299
313
|
(currentMember?.user as { is_account?: boolean } | undefined)?.is_account ??
|
|
@@ -363,7 +377,7 @@ const ChannelViewInner: React.FC<{
|
|
|
363
377
|
<WithComponents overrides={{ Message: MessageOverride }}>
|
|
364
378
|
<Window>
|
|
365
379
|
{/* Custom Channel Header */}
|
|
366
|
-
<div className="p-4">
|
|
380
|
+
<div key="lt-channel-header" className="p-4">
|
|
367
381
|
<CustomChannelHeader
|
|
368
382
|
onBack={onBack}
|
|
369
383
|
showBackButton={showBackButton}
|
|
@@ -371,14 +385,22 @@ const ChannelViewInner: React.FC<{
|
|
|
371
385
|
canShowInfo={Boolean(participant)}
|
|
372
386
|
showStarButton={showStarButton}
|
|
373
387
|
dmAgentEnabled={showDmAgentHeader}
|
|
388
|
+
getParticipantDisplayName={getParticipantDisplayName}
|
|
374
389
|
/>
|
|
375
390
|
</div>
|
|
376
391
|
|
|
377
392
|
{/* Custom Banner/Summary */}
|
|
378
|
-
{renderChannelBanner
|
|
393
|
+
{renderChannelBanner ? (
|
|
394
|
+
<React.Fragment key="lt-channel-banner">
|
|
395
|
+
{renderChannelBanner()}
|
|
396
|
+
</React.Fragment>
|
|
397
|
+
) : null}
|
|
379
398
|
|
|
380
399
|
{/* Message List */}
|
|
381
|
-
<div
|
|
400
|
+
<div
|
|
401
|
+
key="lt-channel-message-list"
|
|
402
|
+
className="flex-1 overflow-hidden relative"
|
|
403
|
+
>
|
|
382
404
|
<MessageList
|
|
383
405
|
hideDeletedMessages
|
|
384
406
|
hideNewMessageSeparator={false}
|
|
@@ -386,10 +408,15 @@ const ChannelViewInner: React.FC<{
|
|
|
386
408
|
/>
|
|
387
409
|
</div>
|
|
388
410
|
|
|
389
|
-
{renderConversationFooter
|
|
411
|
+
{renderConversationFooter ? (
|
|
412
|
+
<React.Fragment key="lt-channel-conversation-footer">
|
|
413
|
+
{renderConversationFooter(channel)}
|
|
414
|
+
</React.Fragment>
|
|
415
|
+
) : null}
|
|
390
416
|
|
|
391
417
|
{/* Message Input */}
|
|
392
418
|
<CustomMessageInput
|
|
419
|
+
key="lt-channel-message-input"
|
|
393
420
|
renderActions={() => renderMessageInputActions?.(channel)}
|
|
394
421
|
/>
|
|
395
422
|
</Window>
|
|
@@ -400,6 +427,7 @@ const ChannelViewInner: React.FC<{
|
|
|
400
427
|
dialogRef={infoDialogRef}
|
|
401
428
|
onClose={handleCloseInfo}
|
|
402
429
|
participant={participant}
|
|
430
|
+
participantDisplayName={getParticipantDisplayName(participant)}
|
|
403
431
|
channel={channel}
|
|
404
432
|
followerStatusLabel={followerStatusLabel}
|
|
405
433
|
onLeaveConversation={onLeaveConversation}
|
|
@@ -444,7 +472,15 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
444
472
|
renderMessage,
|
|
445
473
|
sendButton,
|
|
446
474
|
viewerLanguage,
|
|
475
|
+
getParticipantDisplayName: getParticipantDisplayNameProp,
|
|
447
476
|
}) => {
|
|
477
|
+
const getParticipantDisplayName = useCallback(
|
|
478
|
+
(participant: ChannelMemberResponse | undefined) =>
|
|
479
|
+
getParticipantDisplayNameProp?.(participant) ??
|
|
480
|
+
resolveParticipantDisplayName(participant?.user),
|
|
481
|
+
[getParticipantDisplayNameProp]
|
|
482
|
+
)
|
|
483
|
+
|
|
448
484
|
// Custom send message handler that:
|
|
449
485
|
// 1. Applies messageMetadata if provided
|
|
450
486
|
// 2. Adds skip_push and silent when DM agent is active
|
|
@@ -527,6 +563,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
|
|
|
527
563
|
customChannelActions={customChannelActions}
|
|
528
564
|
renderMessage={renderMessage}
|
|
529
565
|
viewerLanguage={viewerLanguage}
|
|
566
|
+
getParticipantDisplayName={getParticipantDisplayName}
|
|
530
567
|
/>
|
|
531
568
|
</Channel>
|
|
532
569
|
</DmAgentEnabledContext.Provider>
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,11 @@ export {
|
|
|
40
40
|
getMessageDisplayText,
|
|
41
41
|
normalizeLanguageCode,
|
|
42
42
|
} from './utils/getMessageDisplayText'
|
|
43
|
+
export {
|
|
44
|
+
resolveParticipantDisplayName,
|
|
45
|
+
isUuidLike,
|
|
46
|
+
} from './utils/resolveParticipantDisplayName'
|
|
47
|
+
export type { ParticipantDisplayUser } from './utils/resolveParticipantDisplayName'
|
|
43
48
|
|
|
44
49
|
// Types
|
|
45
50
|
export type {
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ComponentType } from 'react'
|
|
|
6
6
|
import type {
|
|
7
7
|
Channel,
|
|
8
8
|
ChannelFilters,
|
|
9
|
+
ChannelMemberResponse,
|
|
9
10
|
ChannelSort,
|
|
10
11
|
LocalMessage,
|
|
11
12
|
SendMessageAPIResponse,
|
|
@@ -183,6 +184,14 @@ export interface ChannelViewProps {
|
|
|
183
184
|
*/
|
|
184
185
|
viewerLanguage?: string
|
|
185
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Resolves the display label for the other channel participant.
|
|
189
|
+
* Defaults to name, then username, then "Unknown member" (never user.id).
|
|
190
|
+
*/
|
|
191
|
+
getParticipantDisplayName?: (
|
|
192
|
+
participant: ChannelMemberResponse | undefined
|
|
193
|
+
) => string
|
|
194
|
+
|
|
186
195
|
/**
|
|
187
196
|
* Custom render function for a banner/card component that renders
|
|
188
197
|
* between the channel header and message list.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isUuidLike,
|
|
5
|
+
resolveParticipantDisplayName,
|
|
6
|
+
} from './resolveParticipantDisplayName'
|
|
7
|
+
|
|
8
|
+
describe('isUuidLike', () => {
|
|
9
|
+
it('returns true for standard UUID strings', () => {
|
|
10
|
+
expect(isUuidLike('a1b2c3d4-e5f6-4789-a012-3456789abcde')).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns false for regular display names', () => {
|
|
14
|
+
expect(isUuidLike('Alice')).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('resolveParticipantDisplayName', () => {
|
|
19
|
+
it('returns a valid name when present', () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolveParticipantDisplayName({
|
|
22
|
+
id: 'user-1',
|
|
23
|
+
name: 'Alice',
|
|
24
|
+
username: 'alice',
|
|
25
|
+
})
|
|
26
|
+
).toBe('Alice')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('prefers username when name is missing', () => {
|
|
30
|
+
expect(
|
|
31
|
+
resolveParticipantDisplayName({
|
|
32
|
+
id: 'user-1',
|
|
33
|
+
username: 'alice',
|
|
34
|
+
})
|
|
35
|
+
).toBe('alice')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('prefers username when name is UUID-like', () => {
|
|
39
|
+
expect(
|
|
40
|
+
resolveParticipantDisplayName({
|
|
41
|
+
id: 'user-1',
|
|
42
|
+
name: 'a1b2c3d4-e5f6-4789-a012-3456789abcde',
|
|
43
|
+
username: 'alice',
|
|
44
|
+
})
|
|
45
|
+
).toBe('alice')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('never falls back to user id', () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveParticipantDisplayName({
|
|
51
|
+
id: 'opaque-user-id',
|
|
52
|
+
name: 'opaque-user-id',
|
|
53
|
+
})
|
|
54
|
+
).toBe('Unknown member')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns Unknown member when no usable fields exist', () => {
|
|
58
|
+
expect(resolveParticipantDisplayName({ id: 'user-1' })).toBe(
|
|
59
|
+
'Unknown member'
|
|
60
|
+
)
|
|
61
|
+
expect(resolveParticipantDisplayName(undefined)).toBe('Unknown member')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('ignores whitespace-only values', () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveParticipantDisplayName({
|
|
67
|
+
id: 'user-1',
|
|
68
|
+
name: ' ',
|
|
69
|
+
username: 'alice',
|
|
70
|
+
})
|
|
71
|
+
).toBe('alice')
|
|
72
|
+
})
|
|
73
|
+
})
|