@linktr.ee/messaging-react 3.1.0 → 3.1.2-rc-1780563075
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-Cq-cN9n1.cjs → Card-0J3_7gmN.cjs} +2 -2
- package/dist/{Card-Cq-cN9n1.cjs.map → Card-0J3_7gmN.cjs.map} +1 -1
- package/dist/{Card-BhO5jeP9.js → Card-CserQom-.js} +2 -2
- package/dist/{Card-BhO5jeP9.js.map → Card-CserQom-.js.map} +1 -1
- package/dist/{Card-NPXVehHb.cjs → Card-DI2viIxR.cjs} +2 -2
- package/dist/{Card-NPXVehHb.cjs.map → Card-DI2viIxR.cjs.map} +1 -1
- package/dist/{Card-BGOWR4lW.js → Card-DOcs__XO.js} +2 -2
- package/dist/{Card-BGOWR4lW.js.map → Card-DOcs__XO.js.map} +1 -1
- package/dist/{Card-CRJ4l5KM.cjs → Card-EqHFqs6U.cjs} +2 -2
- package/dist/{Card-CRJ4l5KM.cjs.map → Card-EqHFqs6U.cjs.map} +1 -1
- package/dist/{Card-BfvsO78k.js → Card-MDGTBRIk.js} +3 -3
- package/dist/{Card-BfvsO78k.js.map → Card-MDGTBRIk.js.map} +1 -1
- package/dist/{LockedThumbnail-B8MKBVXz.cjs → LockedThumbnail-CJTQSHKu.cjs} +2 -2
- package/dist/{LockedThumbnail-B8MKBVXz.cjs.map → LockedThumbnail-CJTQSHKu.cjs.map} +1 -1
- package/dist/{LockedThumbnail-Bu9jNPUi.js → LockedThumbnail-Cw7R8xmf.js} +2 -2
- package/dist/{LockedThumbnail-Bu9jNPUi.js.map → LockedThumbnail-Cw7R8xmf.js.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-Dy7wBqvi.cjs +2 -0
- package/dist/index-Dy7wBqvi.cjs.map +1 -0
- package/dist/{index-CJEl_fID.js → index-UtNoTrrb.js} +666 -665
- package/dist/index-UtNoTrrb.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/src/components/Avatar/index.tsx +1 -4
- package/src/components/ChannelList/CustomChannelPreview.tsx +9 -11
- package/src/components/ChannelView.tsx +14 -15
- package/src/components/CustomMessage/index.tsx +1 -1
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/MessagingShell/MessagingShell.test.tsx +93 -0
- package/src/components/MessagingShell/index.tsx +46 -4
- package/src/providers/MessagingProvider.tsx +1 -1
- package/src/styles.css +53 -10
- package/dist/index-CJEl_fID.js.map +0 -1
- package/dist/index-D-5Igybf.cjs +0 -2
- package/dist/index-D-5Igybf.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-Dy7wBqvi.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.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-UtNoTrrb.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2-rc-1780563075",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"postcss": "^8.4.49",
|
|
61
61
|
"react": "^18.3.1",
|
|
62
62
|
"react-dom": "^18.3.1",
|
|
63
|
+
"sass-embedded": "^1.100.0",
|
|
63
64
|
"storybook": "^8.5.0",
|
|
64
65
|
"stream-chat": "^9.41.1",
|
|
65
66
|
"stream-chat-react": "^13.14.5",
|
|
@@ -43,10 +43,7 @@ export const Avatar = ({
|
|
|
43
43
|
const borderStyle =
|
|
44
44
|
shape === 'circle'
|
|
45
45
|
? { borderRadius: '50%' }
|
|
46
|
-
: {
|
|
47
|
-
borderRadius: '33%',
|
|
48
|
-
cornerShape: 'superellipse(1.3)',
|
|
49
|
-
}
|
|
46
|
+
: { borderRadius: '1rem' }
|
|
50
47
|
|
|
51
48
|
const avatarInner = (
|
|
52
49
|
<div className="h-full w-full overflow-hidden" style={borderStyle}>
|
|
@@ -125,22 +125,21 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
125
125
|
onClick={handleClick}
|
|
126
126
|
onKeyDown={handleKeyDown}
|
|
127
127
|
className={classNames(
|
|
128
|
-
'group w-full px-4 py-3 transition-colors text-left max-w-full overflow-hidden focus-ring',
|
|
128
|
+
'group w-full px-4 py-3 transition-colors text-left max-w-full overflow-hidden focus-ring rounded-[12px] [&+&]:mt-2',
|
|
129
129
|
{
|
|
130
|
-
'bg-
|
|
131
|
-
'hover:bg-
|
|
130
|
+
'bg-black/[0.04]': isSelected,
|
|
131
|
+
'hover:bg-black/[0.02]': !isSelected,
|
|
132
132
|
}
|
|
133
133
|
)}
|
|
134
134
|
>
|
|
135
|
-
<div className="flex items-start gap-
|
|
135
|
+
<div className="flex items-start gap-4">
|
|
136
136
|
{/* Avatar */}
|
|
137
137
|
<Avatar
|
|
138
138
|
id={participant?.user?.id || channel.id || 'unknown'}
|
|
139
139
|
name={participantName}
|
|
140
140
|
image={participantImage}
|
|
141
|
-
size={
|
|
141
|
+
size={48}
|
|
142
142
|
starred={isChannelStarred}
|
|
143
|
-
className="[&_.avatar-fallback]:group-hover:bg-[#eeeeee]"
|
|
144
143
|
/>
|
|
145
144
|
|
|
146
145
|
{/* Content column */}
|
|
@@ -149,8 +148,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
149
148
|
<div className="flex items-center justify-between gap-2">
|
|
150
149
|
<h3
|
|
151
150
|
className={classNames(
|
|
152
|
-
'text-sm font-medium truncate'
|
|
153
|
-
isSelected ? 'text-primary' : 'text-charcoal'
|
|
151
|
+
'text-sm font-medium truncate text-[#191918]'
|
|
154
152
|
)}
|
|
155
153
|
>
|
|
156
154
|
{isChannelStarred && (
|
|
@@ -159,7 +157,7 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
159
157
|
{participantName}
|
|
160
158
|
</h3>
|
|
161
159
|
{lastMessageTime && (
|
|
162
|
-
<span className="text-xs text-
|
|
160
|
+
<span className="text-xs text-[#717070] flex-shrink-0">
|
|
163
161
|
{lastMessageTime}
|
|
164
162
|
</span>
|
|
165
163
|
)}
|
|
@@ -167,11 +165,11 @@ const CustomChannelPreview = React.memo<ChannelPreviewUIComponentProps>(
|
|
|
167
165
|
|
|
168
166
|
{/* Message and unread badge row */}
|
|
169
167
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
170
|
-
<p className="text-
|
|
168
|
+
<p className="text-sm text-[#717070] flex-1 line-clamp-1">
|
|
171
169
|
{messagePreview}
|
|
172
170
|
</p>
|
|
173
171
|
{unreadCount > 0 && (
|
|
174
|
-
<span className="bg-[#7f22fe] text-white text-
|
|
172
|
+
<span className="bg-[#7f22fe] text-white text-[10px] rounded-full h-4 flex items-center justify-center p-1 min-w-4 text-center flex-shrink-0">
|
|
175
173
|
{unreadCount > 99 ? '99+' : unreadCount}
|
|
176
174
|
</span>
|
|
177
175
|
)}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowLeftIcon,
|
|
3
|
-
CaretRightIcon,
|
|
4
3
|
DotsThreeIcon,
|
|
5
4
|
SparkleIcon,
|
|
6
5
|
StarIcon,
|
|
@@ -35,7 +34,7 @@ import { ChannelEmptyState } from './MessagingShell/ChannelEmptyState'
|
|
|
35
34
|
import { LoadingState } from './MessagingShell/LoadingState'
|
|
36
35
|
|
|
37
36
|
const ICON_BTN_CLASS =
|
|
38
|
-
'size-10 rounded-full
|
|
37
|
+
'size-10 rounded-full hover:bg-[#E5E4E1] flex items-center justify-center transition-colors duration-150 focus-ring'
|
|
39
38
|
|
|
40
39
|
const DM_AGENT_HEADER_HELPER_TEXT = 'Replies instantly with AI assistant'
|
|
41
40
|
|
|
@@ -90,13 +89,13 @@ const CustomChannelHeader: React.FC<{
|
|
|
90
89
|
|
|
91
90
|
return (
|
|
92
91
|
<div className="@container">
|
|
93
|
-
<div className="grid grid-cols-[1fr_auto_1fr] w-full items-center @lg:hidden">
|
|
92
|
+
<div className="grid grid-cols-[1fr_auto_1fr] w-full items-center @lg:hidden px-6 py-3">
|
|
94
93
|
<div className="flex items-center gap-2">
|
|
95
94
|
{showBackButton && (
|
|
96
95
|
<button
|
|
97
96
|
className={classNames(
|
|
98
97
|
ICON_BTN_CLASS,
|
|
99
|
-
'messaging-channel-view-back-button-mobile'
|
|
98
|
+
'messaging-channel-view-back-button-mobile bg-[#F1F0EE]'
|
|
100
99
|
)}
|
|
101
100
|
onClick={onBack || (() => {})}
|
|
102
101
|
type="button"
|
|
@@ -113,16 +112,15 @@ const CustomChannelHeader: React.FC<{
|
|
|
113
112
|
image={participantImage}
|
|
114
113
|
starred={showStarButton && isStarred}
|
|
115
114
|
dmAgentEnabled={dmAgentEnabled}
|
|
116
|
-
size={
|
|
115
|
+
size={48}
|
|
117
116
|
/>
|
|
118
117
|
<button
|
|
119
118
|
type="button"
|
|
120
119
|
onClick={onShowInfo}
|
|
121
|
-
className="flex items-center gap-0.5 rounded-full
|
|
120
|
+
className="flex items-center gap-0.5 rounded-full px-3 py-1 text-xs font-medium text-black/90 hover:bg-black/[0.08] transition-colors"
|
|
122
121
|
aria-label={`View info for ${participantName}`}
|
|
123
122
|
>
|
|
124
123
|
{participantName}
|
|
125
|
-
<CaretRightIcon className="size-3 shrink-0" />
|
|
126
124
|
</button>
|
|
127
125
|
{dmAgentEnabled && (
|
|
128
126
|
<div className="flex items-center gap-1 text-[10px] leading-3 text-black/55">
|
|
@@ -151,7 +149,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
151
149
|
</button>
|
|
152
150
|
)}
|
|
153
151
|
<button
|
|
154
|
-
className={ICON_BTN_CLASS}
|
|
152
|
+
className={classNames(ICON_BTN_CLASS, 'bg-[#F1F0EE]')}
|
|
155
153
|
onClick={onShowInfo}
|
|
156
154
|
type="button"
|
|
157
155
|
aria-label="Show info"
|
|
@@ -160,7 +158,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
160
158
|
</button>
|
|
161
159
|
</div>
|
|
162
160
|
</div>
|
|
163
|
-
<div className="hidden @lg:flex items-center justify-between gap-3 min-h-12">
|
|
161
|
+
<div className="px-6 py-3 hidden @lg:flex items-center justify-between gap-3 min-h-12 border-b border-b-black/[0.08]">
|
|
164
162
|
<div className="flex items-center gap-4 min-w-0">
|
|
165
163
|
{showBackButton && onBack && (
|
|
166
164
|
<button
|
|
@@ -182,7 +180,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
182
180
|
image={participantImage}
|
|
183
181
|
starred={showStarButton && isStarred}
|
|
184
182
|
dmAgentEnabled={dmAgentEnabled}
|
|
185
|
-
size={
|
|
183
|
+
size={48}
|
|
186
184
|
/>
|
|
187
185
|
<div className="min-w-0">
|
|
188
186
|
{canShowInfo ? (
|
|
@@ -193,7 +191,6 @@ const CustomChannelHeader: React.FC<{
|
|
|
193
191
|
aria-label={`View info for ${participantName}`}
|
|
194
192
|
>
|
|
195
193
|
<span className="truncate">{participantName}</span>
|
|
196
|
-
<CaretRightIcon className="size-4 shrink-0" />
|
|
197
194
|
</button>
|
|
198
195
|
) : (
|
|
199
196
|
<h1 className="font-medium text-black/90 truncate">
|
|
@@ -219,7 +216,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
219
216
|
}
|
|
220
217
|
>
|
|
221
218
|
<StarIcon
|
|
222
|
-
className={classNames('size-
|
|
219
|
+
className={classNames('size-6', {
|
|
223
220
|
'text-yellow-600': isStarred,
|
|
224
221
|
'text-black/90': !isStarred,
|
|
225
222
|
})}
|
|
@@ -234,7 +231,7 @@ const CustomChannelHeader: React.FC<{
|
|
|
234
231
|
type="button"
|
|
235
232
|
aria-label="Show info"
|
|
236
233
|
>
|
|
237
|
-
<DotsThreeIcon className="size-
|
|
234
|
+
<DotsThreeIcon className="size-6 text-black/90" />
|
|
238
235
|
</button>
|
|
239
236
|
)}
|
|
240
237
|
</div>
|
|
@@ -398,7 +395,7 @@ const ChannelViewInner: React.FC<{
|
|
|
398
395
|
>
|
|
399
396
|
<Window>
|
|
400
397
|
{/* Custom Channel Header */}
|
|
401
|
-
<div key="lt-channel-header"
|
|
398
|
+
<div key="lt-channel-header">
|
|
402
399
|
<CustomChannelHeader
|
|
403
400
|
onBack={onBack}
|
|
404
401
|
showBackButton={showBackButton}
|
|
@@ -436,7 +433,9 @@ const ChannelViewInner: React.FC<{
|
|
|
436
433
|
{/* Message Input */}
|
|
437
434
|
<CustomMessageInput
|
|
438
435
|
key="lt-channel-message-input"
|
|
439
|
-
|
|
436
|
+
{...(renderMessageInputActions && {
|
|
437
|
+
renderActions: () => renderMessageInputActions?.(channel),
|
|
438
|
+
})}
|
|
440
439
|
renderFooter={() => renderMessageInputFooter?.(channel)}
|
|
441
440
|
disabled={composerDisabled}
|
|
442
441
|
disabledReason={composerDisabledReason}
|
|
@@ -207,7 +207,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
207
207
|
id={message.user.id}
|
|
208
208
|
image={message.user.image}
|
|
209
209
|
name={message.user.name || message.user.id}
|
|
210
|
-
size={
|
|
210
|
+
size={24}
|
|
211
211
|
shape="circle"
|
|
212
212
|
dmAgentEnabled={isChatbot}
|
|
213
213
|
/>
|
|
@@ -60,7 +60,7 @@ const CustomMessageInputInner: React.FC = () => {
|
|
|
60
60
|
<div className="w-full ml-2 mr-4 self-center leading-[0]">
|
|
61
61
|
<TextareaComposer
|
|
62
62
|
aria-disabled={disabled || undefined}
|
|
63
|
-
className="w-full resize-none outline-none leading-
|
|
63
|
+
className="w-full resize-none outline-none leading-5 placeholder:text-black/30 text-sm"
|
|
64
64
|
// While this might usually be considered an anti-pattern, in most
|
|
65
65
|
// cases, when a message thread is rendered, we want the input to
|
|
66
66
|
// gain focus automatically.
|
|
@@ -168,4 +168,97 @@ describe('MessagingShell', () => {
|
|
|
168
168
|
'Conversation ended'
|
|
169
169
|
)
|
|
170
170
|
})
|
|
171
|
+
|
|
172
|
+
it('does not create the channel again when the load effect re-runs for the same pair (e.g. service identity settles during connect)', async () => {
|
|
173
|
+
queryChannelsMock.mockResolvedValue([])
|
|
174
|
+
startChannelWithParticipantMock.mockResolvedValue(makeChannel('created-1'))
|
|
175
|
+
|
|
176
|
+
const { rerender } = renderWithProviders(
|
|
177
|
+
<MessagingShell
|
|
178
|
+
initialParticipantFilter="other-1"
|
|
179
|
+
initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await waitFor(() =>
|
|
184
|
+
expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// A fresh `service` reference (as happens while the connection settles)
|
|
188
|
+
// re-runs the load effect. The once-per-pair guard must prevent a second
|
|
189
|
+
// create for the same viewer/participant pair before the new channel is
|
|
190
|
+
// indexed by queryChannels.
|
|
191
|
+
useMessagingReturn = {
|
|
192
|
+
...useMessagingReturn,
|
|
193
|
+
service: { startChannelWithParticipant: startChannelWithParticipantMock },
|
|
194
|
+
}
|
|
195
|
+
rerender(
|
|
196
|
+
<MessagingShell
|
|
197
|
+
initialParticipantFilter="other-1"
|
|
198
|
+
initialParticipantData={{ id: 'other-1', name: 'Other Person' }}
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
203
|
+
expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('does not wipe a newer pair load guard when an earlier in-flight load fails', async () => {
|
|
207
|
+
const PAIR_A = 'pair-A'
|
|
208
|
+
const PAIR_B = 'pair-B'
|
|
209
|
+
|
|
210
|
+
// Pair A's query stays pending until we reject it, simulating a load still
|
|
211
|
+
// in flight when the participant changes. Pair B resolves with no channel.
|
|
212
|
+
let rejectA: (reason?: unknown) => void = () => {}
|
|
213
|
+
queryChannelsMock.mockImplementation(
|
|
214
|
+
(query: { members?: { $eq?: string[] } }) => {
|
|
215
|
+
if (query?.members?.$eq?.includes(PAIR_A)) {
|
|
216
|
+
return new Promise((_resolve, reject) => {
|
|
217
|
+
rejectA = reject
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
return Promise.resolve([])
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
startChannelWithParticipantMock.mockResolvedValue(makeChannel('created-b'))
|
|
224
|
+
|
|
225
|
+
const { rerender } = renderWithProviders(
|
|
226
|
+
<MessagingShell
|
|
227
|
+
initialParticipantFilter={PAIR_A}
|
|
228
|
+
initialParticipantData={{ id: PAIR_A, name: 'A' }}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
// Switch to pair B while A's load is still pending; B creates its channel.
|
|
233
|
+
rerender(
|
|
234
|
+
<MessagingShell
|
|
235
|
+
initialParticipantFilter={PAIR_B}
|
|
236
|
+
initialParticipantData={{ id: PAIR_B, name: 'B' }}
|
|
237
|
+
/>
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
await waitFor(() =>
|
|
241
|
+
expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// A's stale load now fails. Its catch must not clear pair B's guard.
|
|
245
|
+
rejectA(new Error('transient'))
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
247
|
+
|
|
248
|
+
// Re-run the load effect for pair B (a fresh `service` reference). With B's
|
|
249
|
+
// guard intact this must not create a second channel.
|
|
250
|
+
useMessagingReturn = {
|
|
251
|
+
...useMessagingReturn,
|
|
252
|
+
service: { startChannelWithParticipant: startChannelWithParticipantMock },
|
|
253
|
+
}
|
|
254
|
+
rerender(
|
|
255
|
+
<MessagingShell
|
|
256
|
+
initialParticipantFilter={PAIR_B}
|
|
257
|
+
initialParticipantData={{ id: PAIR_B, name: 'B' }}
|
|
258
|
+
/>
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
262
|
+
expect(startChannelWithParticipantMock).toHaveBeenCalledTimes(1)
|
|
263
|
+
})
|
|
171
264
|
})
|
|
@@ -65,13 +65,44 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
65
65
|
const onChannelSelectRef = useRef(onChannelSelect)
|
|
66
66
|
onChannelSelectRef.current = onChannelSelect
|
|
67
67
|
|
|
68
|
+
// Track the direct-conversation load to prevent repeated/concurrent loads.
|
|
69
|
+
// Identity-stable deps (above) stop the common re-fire, but legitimate dep
|
|
70
|
+
// changes (e.g. `service`/`client` settling during connect) can still re-run
|
|
71
|
+
// this effect before the just-created channel is indexed, each firing another
|
|
72
|
+
// startChannelWithParticipant call and creating duplicate welcome messages.
|
|
73
|
+
const directConversationLoadRef = useRef<string | null>(null)
|
|
74
|
+
|
|
75
|
+
// Mirror the currently-selected channel into a ref so the async load can read
|
|
76
|
+
// the latest value rather than the (possibly stale) effect-closure value.
|
|
77
|
+
const selectedChannelRef = useRef<Channel | null>(null)
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
selectedChannelRef.current = selectedChannel
|
|
80
|
+
}, [selectedChannel])
|
|
81
|
+
|
|
68
82
|
useEffect(() => {
|
|
69
83
|
if (!client || !isConnected) return
|
|
70
84
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
const userId = client.userID
|
|
86
|
+
if (!userId) return
|
|
87
|
+
|
|
88
|
+
// Only load once per viewer ↔ participant pair. Set synchronously (before
|
|
89
|
+
// the async work) so re-runs triggered by changing dependency identities
|
|
90
|
+
// bail out instead of issuing another channel-create request.
|
|
91
|
+
const loadKey = `${userId}::${initialParticipantFilter}`
|
|
92
|
+
if (directConversationLoadRef.current === loadKey) return
|
|
93
|
+
directConversationLoadRef.current = loadKey
|
|
74
94
|
|
|
95
|
+
// Release the guard only if it still belongs to this load. A newer load for
|
|
96
|
+
// a different pair may have taken ownership while this one was in flight, so
|
|
97
|
+
// clearing unconditionally could wipe the newer guard and let a duplicate
|
|
98
|
+
// load/create slip through.
|
|
99
|
+
const releaseLoadGuard = () => {
|
|
100
|
+
if (directConversationLoadRef.current === loadKey) {
|
|
101
|
+
directConversationLoadRef.current = null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const loadInitialChannel = async () => {
|
|
75
106
|
try {
|
|
76
107
|
if (debug) {
|
|
77
108
|
console.log(
|
|
@@ -105,6 +136,8 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
105
136
|
|
|
106
137
|
const participantData = initialParticipantDataRef.current
|
|
107
138
|
if (!participantData || !service) {
|
|
139
|
+
// Allow a retry once participant data / service become available.
|
|
140
|
+
releaseLoadGuard()
|
|
108
141
|
setDirectConversationError('No conversation found with this account')
|
|
109
142
|
if (debug) {
|
|
110
143
|
console.log(
|
|
@@ -136,6 +169,8 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
136
169
|
'[MessagingShell] Failed to create conversation:',
|
|
137
170
|
createErr
|
|
138
171
|
)
|
|
172
|
+
// Allow a retry for this pair after a transient failure.
|
|
173
|
+
releaseLoadGuard()
|
|
139
174
|
setDirectConversationError('Failed to create conversation')
|
|
140
175
|
}
|
|
141
176
|
} catch (err) {
|
|
@@ -143,7 +178,14 @@ export const MessagingShell: React.FC<MessagingShellProps> = ({
|
|
|
143
178
|
'[MessagingShell] Failed to load initial conversation:',
|
|
144
179
|
err
|
|
145
180
|
)
|
|
146
|
-
|
|
181
|
+
// Allow a retry for this pair after a transient failure.
|
|
182
|
+
releaseLoadGuard()
|
|
183
|
+
// Don't replace an already-loaded conversation with an error screen.
|
|
184
|
+
// Read the latest selected channel via ref to avoid acting on a stale
|
|
185
|
+
// closure value when the channel was selected mid-flight.
|
|
186
|
+
if (!selectedChannelRef.current) {
|
|
187
|
+
setDirectConversationError('Failed to load conversation')
|
|
188
|
+
}
|
|
147
189
|
}
|
|
148
190
|
}
|
|
149
191
|
|
|
@@ -340,7 +340,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
|
|
|
340
340
|
client={client}
|
|
341
341
|
customClasses={{
|
|
342
342
|
channelList:
|
|
343
|
-
'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A]',
|
|
343
|
+
'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A] p-4',
|
|
344
344
|
}}
|
|
345
345
|
>
|
|
346
346
|
{children}
|
package/src/styles.css
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
@layer stream, stream-overrides;
|
|
2
|
+
|
|
1
3
|
/* Stream Chat base styles */
|
|
2
|
-
@import 'stream-chat-react/dist/
|
|
4
|
+
@import 'stream-chat-react/dist/scss/v2/index.scss' layer(stream);
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
@layer stream-overrides {
|
|
7
|
+
.str-chat {
|
|
8
|
+
/* Inherit the host's font instead of stream-chat-react's hardcoded system stack.
|
|
5
9
|
In admin (federation host) this resolves to Link Sans; on linktr.ee profile
|
|
6
10
|
pages it resolves to the creator's profile font. */
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
--str-chat__font-family: inherit;
|
|
12
|
+
|
|
13
|
+
--str-chat__message-bubble-border-radius: 1.5rem;
|
|
14
|
+
--str-chat__message-bubble-background-color: #f1f0ee;
|
|
15
|
+
--str-chat__message-bubble-color: #181818;
|
|
16
|
+
|
|
17
|
+
--str-chat__own-message-bubble-background-color: #121110;
|
|
18
|
+
--str-chat__own-message-bubble-color: #fff;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.str-chat__message.str-chat__message--other {
|
|
22
|
+
column-gap: 0.25rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.str-chat__message-text {
|
|
26
|
+
padding: 0.75rem 1rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.str-chat__message-bubble {
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
line-height: 1.25rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.str-chat__li--single,
|
|
35
|
+
.str-chat__li--bottom {
|
|
36
|
+
margin-block-end: 0.875rem;
|
|
37
|
+
|
|
38
|
+
.str-chat__message--other .str-chat__message-bubble {
|
|
39
|
+
border-end-start-radius: var(--str-chat__message-bubble-border-radius);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.str-chat__message--me .str-chat__message-bubble {
|
|
43
|
+
border-end-end-radius: var(--str-chat__message-bubble-border-radius);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.str-chat__date-separator {
|
|
48
|
+
.str-chat__date-separator-line {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.str-chat__date-separator-date {
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
font-size: 0.75rem;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
9
57
|
}
|
|
10
58
|
|
|
11
59
|
/* Dialog component styles - used by messaging components */
|
|
@@ -293,11 +341,6 @@
|
|
|
293
341
|
}
|
|
294
342
|
|
|
295
343
|
.str-chat__message.str-chat__message--me {
|
|
296
|
-
.str-chat__message-bubble {
|
|
297
|
-
background-color: #121110;
|
|
298
|
-
color: white;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
344
|
.str-chat__attachment-list .str-chat__message-attachment--card {
|
|
302
345
|
color: white;
|
|
303
346
|
}
|
|
@@ -594,7 +637,7 @@
|
|
|
594
637
|
.str-chat__message-text
|
|
595
638
|
.str-chat__message-text-inner
|
|
596
639
|
p {
|
|
597
|
-
margin: 0
|
|
640
|
+
margin: 0;
|
|
598
641
|
}
|
|
599
642
|
|
|
600
643
|
.str-chat__message
|