@ermis-network/ermis-chat-react 2.0.0 → 2.0.1
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/README.md +144 -0
- package/dist/index.cjs +5087 -11279
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +632 -152
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +273 -9
- package/dist/index.d.ts +273 -9
- package/dist/index.mjs +5085 -11295
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/Channel.tsx +0 -3
- package/src/components/ChannelActions.tsx +6 -1
- package/src/components/ChannelHeader.tsx +8 -32
- package/src/components/ChannelInfo/AddMemberModal.tsx +7 -1
- package/src/components/ChannelInfo/ChannelInfo.tsx +82 -2
- package/src/components/ChannelInfo/EditChannelModal.tsx +2 -2
- package/src/components/ChannelInfo/MediaGridItem.tsx +215 -78
- package/src/components/ChannelInfo/useChannelInfoTabs.tsx +170 -129
- package/src/components/ChannelList.tsx +72 -13
- package/src/components/CreateChannelModal.tsx +131 -12
- package/src/components/FilesPreview.tsx +8 -12
- package/src/components/FlatTopicGroupItem.tsx +27 -16
- package/src/components/ForwardMessageModal.tsx +11 -3
- package/src/components/MediaLightbox.tsx +444 -304
- package/src/components/MessageActionsBox.tsx +2 -0
- package/src/components/MessageInput.tsx +41 -12
- package/src/components/MessageItem.tsx +70 -25
- package/src/components/MessageQuickReactions.tsx +131 -128
- package/src/components/MessageReactions.tsx +47 -2
- package/src/components/MessageRenderers.tsx +1030 -433
- package/src/components/PinnedMessages.tsx +40 -12
- package/src/components/QuotedMessagePreview.tsx +99 -8
- package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
- package/src/components/RecoveryPin/index.ts +19 -0
- package/src/components/TopicList.tsx +20 -5
- package/src/components/TypingIndicator.tsx +3 -3
- package/src/components/UserPicker.tsx +26 -25
- package/src/components/VirtualMessageList.tsx +345 -125
- package/src/context/ChatProvider.tsx +27 -1
- package/src/hooks/useChannelListUpdates.ts +22 -1
- package/src/hooks/useChannelMessages.ts +338 -51
- package/src/hooks/useChannelRowUpdates.ts +18 -6
- package/src/hooks/useChatUser.ts +9 -1
- package/src/hooks/useE2eeAttachmentRenderer.ts +204 -0
- package/src/hooks/useE2eeFileUpload.ts +38 -0
- package/src/hooks/useFileUpload.ts +25 -5
- package/src/hooks/useForwardMessage.ts +210 -13
- package/src/hooks/useLoadMessages.ts +16 -4
- package/src/hooks/useMentions.ts +60 -6
- package/src/hooks/useMessageActions.ts +14 -8
- package/src/hooks/useMessageSend.ts +64 -12
- package/src/hooks/usePendingE2eeSends.ts +29 -0
- package/src/hooks/useRecoveryPin.ts +287 -0
- package/src/hooks/useScrollToMessage.ts +29 -4
- package/src/hooks/useTopicGroupUpdates.ts +49 -11
- package/src/index.ts +23 -0
- package/src/messageTypeUtils.ts +14 -0
- package/src/styles/_channel-info.css +9 -0
- package/src/styles/_channel-list.css +37 -14
- package/src/styles/_media-lightbox.css +36 -3
- package/src/styles/_message-bubble.css +381 -41
- package/src/styles/_message-input.css +8 -0
- package/src/styles/_message-list.css +67 -10
- package/src/styles/_message-quick-reactions.css +101 -59
- package/src/styles/_message-reactions.css +18 -32
- package/src/styles/_recovery-pin.css +97 -0
- package/src/styles/_tokens.css +5 -5
- package/src/styles/_typing-indicator.css +23 -13
- package/src/styles/index.css +1 -0
- package/src/types.ts +115 -1
- package/src/utils/avatarColors.ts +1 -1
- package/src/utils.ts +38 -18
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
canRemoveTargetMember,
|
|
16
16
|
canBanTargetMember,
|
|
17
17
|
canPromoteTargetMember,
|
|
18
|
-
canDemoteTargetMember
|
|
18
|
+
canDemoteTargetMember,
|
|
19
19
|
} from '../../channelRoleUtils';
|
|
20
20
|
|
|
21
21
|
export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
@@ -52,7 +52,7 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
52
52
|
const availableTabs: MediaTab[] = useMemo(() => {
|
|
53
53
|
let tabs = isMessaging ? MESSAGING_TABS : ALL_TABS;
|
|
54
54
|
if (isTopic) {
|
|
55
|
-
tabs = tabs.filter(t => t !== 'members');
|
|
55
|
+
tabs = tabs.filter((t) => t !== 'members');
|
|
56
56
|
}
|
|
57
57
|
return tabs;
|
|
58
58
|
}, [isMessaging, isTopic]);
|
|
@@ -65,31 +65,34 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
65
65
|
const lastFetchedCidRef = useRef<string | null>(null);
|
|
66
66
|
const transitionRafRef = useRef<any>(null);
|
|
67
67
|
|
|
68
|
-
const handleTabChange = useCallback(
|
|
69
|
-
|
|
68
|
+
const handleTabChange = useCallback(
|
|
69
|
+
(tab: MediaTab) => {
|
|
70
|
+
if (tab === activeTab) return;
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
// 1. Instant UI update for the tab button
|
|
73
|
+
setActiveTab(tab);
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
if (transitionRafRef.current) clearTimeout(transitionRafRef.current);
|
|
76
|
+
|
|
77
|
+
// Check if data is already available for this channel
|
|
78
|
+
const hasData = tab === 'members' || attachmentsFetchedForCid === channel?.cid;
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (hasData) {
|
|
80
|
-
// If data exists, switch content immediately without loading state
|
|
81
|
-
setContentTab(tab);
|
|
82
|
-
setIsPending(false);
|
|
83
|
-
} else {
|
|
84
|
-
// If no data, use isPending to show Skeleton while waiting for API
|
|
85
|
-
setIsPending(true);
|
|
86
|
-
transitionRafRef.current = setTimeout(() => {
|
|
80
|
+
if (hasData) {
|
|
81
|
+
// If data exists, switch content immediately without loading state
|
|
87
82
|
setContentTab(tab);
|
|
88
83
|
setIsPending(false);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
} else {
|
|
85
|
+
// If no data, use isPending to show Skeleton while waiting for API
|
|
86
|
+
setIsPending(true);
|
|
87
|
+
transitionRafRef.current = setTimeout(() => {
|
|
88
|
+
setContentTab(tab);
|
|
89
|
+
setIsPending(false);
|
|
90
|
+
setAttachmentsFetchedForCid((prev) => prev || channel?.cid || null);
|
|
91
|
+
}, 350);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[activeTab, channel?.cid, attachmentsFetchedForCid],
|
|
95
|
+
);
|
|
93
96
|
|
|
94
97
|
// Reset tab when user switches channels
|
|
95
98
|
useEffect(() => {
|
|
@@ -128,7 +131,7 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
128
131
|
|
|
129
132
|
const forceRefreshAttachments = useCallback(() => {
|
|
130
133
|
lastFetchedCidRef.current = null;
|
|
131
|
-
setRefreshAttachmentsCount(c => c + 1);
|
|
134
|
+
setRefreshAttachmentsCount((c) => c + 1);
|
|
132
135
|
}, []);
|
|
133
136
|
|
|
134
137
|
const sortedMembers = useMemo(() => {
|
|
@@ -140,19 +143,16 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
140
143
|
}, [members]);
|
|
141
144
|
|
|
142
145
|
// Categorize attachments by type
|
|
143
|
-
const mediaItems = useMemo(
|
|
144
|
-
allAttachments.filter(a => a.attachment_type === 'image' || a.attachment_type === 'video'),
|
|
145
|
-
[allAttachments]
|
|
146
|
+
const mediaItems = useMemo(
|
|
147
|
+
() => allAttachments.filter((a) => a.attachment_type === 'image' || a.attachment_type === 'video'),
|
|
148
|
+
[allAttachments],
|
|
146
149
|
);
|
|
147
150
|
|
|
148
|
-
const linkItems = useMemo(() =>
|
|
149
|
-
allAttachments.filter(a => a.attachment_type === 'linkPreview'),
|
|
150
|
-
[allAttachments]
|
|
151
|
-
);
|
|
151
|
+
const linkItems = useMemo(() => allAttachments.filter((a) => a.attachment_type === 'linkPreview'), [allAttachments]);
|
|
152
152
|
|
|
153
|
-
const fileItems = useMemo(
|
|
154
|
-
allAttachments.filter(a => a.attachment_type === 'file' || a.attachment_type === 'voiceRecording'),
|
|
155
|
-
[allAttachments]
|
|
153
|
+
const fileItems = useMemo(
|
|
154
|
+
() => allAttachments.filter((a) => a.attachment_type === 'file' || a.attachment_type === 'voiceRecording'),
|
|
155
|
+
[allAttachments],
|
|
156
156
|
);
|
|
157
157
|
|
|
158
158
|
useEffect(() => {
|
|
@@ -183,7 +183,7 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
183
183
|
lastFetchedCidRef.current = channel?.cid || null;
|
|
184
184
|
}
|
|
185
185
|
} catch (err) {
|
|
186
|
-
console.error(
|
|
186
|
+
console.error('Failed to query media for channel info', err);
|
|
187
187
|
if (active) setAllAttachments([]);
|
|
188
188
|
} finally {
|
|
189
189
|
if (active) setLoading(false);
|
|
@@ -191,7 +191,9 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
191
191
|
};
|
|
192
192
|
|
|
193
193
|
fetchMedia();
|
|
194
|
-
return () => {
|
|
194
|
+
return () => {
|
|
195
|
+
active = false;
|
|
196
|
+
};
|
|
195
197
|
}, [channel, isBanned, isBlocked, isPreviewMode, attachmentsFetchedForCid, isVisible, refreshAttachmentsCount]);
|
|
196
198
|
|
|
197
199
|
// Listen to realtime events to automatically refresh attachments
|
|
@@ -217,30 +219,40 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
217
219
|
|
|
218
220
|
const { downloadFile } = useDownloadHandler();
|
|
219
221
|
|
|
220
|
-
const handleDownloadFile = useCallback(
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
const handleDownloadFile = useCallback(
|
|
223
|
+
async (url: string, filename?: string) => {
|
|
224
|
+
await downloadFile(url, filename);
|
|
225
|
+
},
|
|
226
|
+
[downloadFile],
|
|
227
|
+
);
|
|
223
228
|
|
|
224
229
|
// Lightbox state for media tab
|
|
225
230
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
226
231
|
const [lightboxIndex, setLightboxIndex] = useState(0);
|
|
227
232
|
|
|
228
233
|
const lightboxItems = useMemo<MediaLightboxItem[]>(() => {
|
|
229
|
-
return mediaItems
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
return mediaItems
|
|
235
|
+
.filter((item) => !item.e2ee_manifest && !item.e2ee_manifest_missing)
|
|
236
|
+
.map((item) => ({
|
|
237
|
+
type: (item.attachment_type === 'video' ? 'video' : 'image') as 'image' | 'video',
|
|
238
|
+
src: item.url,
|
|
239
|
+
alt: item.file_name,
|
|
240
|
+
posterSrc: item.thumb_url || undefined,
|
|
241
|
+
}));
|
|
235
242
|
}, [mediaItems]);
|
|
236
243
|
|
|
237
|
-
const handleMediaClick = useCallback(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
const handleMediaClick = useCallback(
|
|
245
|
+
(url: string) => {
|
|
246
|
+
const idx = mediaItems
|
|
247
|
+
.filter((item) => !item.e2ee_manifest && !item.e2ee_manifest_missing)
|
|
248
|
+
.findIndex((item) => item.url === url);
|
|
249
|
+
if (idx >= 0) {
|
|
250
|
+
setLightboxIndex(idx);
|
|
251
|
+
setLightboxOpen(true);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[mediaItems],
|
|
255
|
+
);
|
|
244
256
|
|
|
245
257
|
const closeLightbox = useCallback(() => {
|
|
246
258
|
setLightboxOpen(false);
|
|
@@ -263,103 +275,132 @@ export const useChannelInfoTabs = (props: ChannelInfoTabsProps) => {
|
|
|
263
275
|
if (onAddMemberClick) {
|
|
264
276
|
items.push({ type: 'add-member' });
|
|
265
277
|
}
|
|
266
|
-
sortedMembers.forEach(member => {
|
|
278
|
+
sortedMembers.forEach((member) => {
|
|
267
279
|
items.push({ type: 'member', data: member });
|
|
268
280
|
});
|
|
269
281
|
return items;
|
|
270
282
|
}
|
|
271
283
|
case 'media':
|
|
272
|
-
return mediaRows.map(row => ({ type: 'media-row', data: row }));
|
|
284
|
+
return mediaRows.map((row) => ({ type: 'media-row', data: row }));
|
|
273
285
|
case 'links':
|
|
274
|
-
return linkItems.map(item => ({ type: 'link', data: item }));
|
|
286
|
+
return linkItems.map((item) => ({ type: 'link', data: item }));
|
|
275
287
|
case 'files':
|
|
276
|
-
return fileItems.map(item => ({ type: 'file', data: item }));
|
|
288
|
+
return fileItems.map((item) => ({ type: 'file', data: item }));
|
|
277
289
|
default:
|
|
278
290
|
return [];
|
|
279
291
|
}
|
|
280
292
|
}, [contentTab, sortedMembers, mediaRows, mediaItems, linkItems, fileItems, onAddMemberClick]);
|
|
281
293
|
|
|
282
294
|
// Render function for VList items
|
|
283
|
-
const renderVlistItem = useCallback(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
const renderVlistItem = useCallback(
|
|
296
|
+
(item: any, index: number) => {
|
|
297
|
+
switch (item.type) {
|
|
298
|
+
case 'add-member':
|
|
299
|
+
if (AddMemberButtonComponent) {
|
|
300
|
+
return (
|
|
301
|
+
<div key="__add-member__" className="ermis-channel-info__add-member-wrap">
|
|
302
|
+
<AddMemberButtonComponent onClick={onAddMemberClick!} label={addMemberButtonLabel} />
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
287
306
|
return (
|
|
288
307
|
<div key="__add-member__" className="ermis-channel-info__add-member-wrap">
|
|
289
|
-
<
|
|
308
|
+
<button className="ermis-channel-info__add-member-btn" onClick={onAddMemberClick}>
|
|
309
|
+
<svg
|
|
310
|
+
width="18"
|
|
311
|
+
height="18"
|
|
312
|
+
viewBox="0 0 24 24"
|
|
313
|
+
fill="none"
|
|
314
|
+
stroke="currentColor"
|
|
315
|
+
strokeWidth="2"
|
|
316
|
+
strokeLinecap="round"
|
|
317
|
+
strokeLinejoin="round"
|
|
318
|
+
>
|
|
319
|
+
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
320
|
+
<circle cx="8.5" cy="7" r="4"></circle>
|
|
321
|
+
<line x1="20" y1="8" x2="20" y2="14"></line>
|
|
322
|
+
<line x1="23" y1="11" x2="17" y2="11"></line>
|
|
323
|
+
</svg>
|
|
324
|
+
{addMemberButtonLabel}
|
|
325
|
+
</button>
|
|
290
326
|
</div>
|
|
291
327
|
);
|
|
328
|
+
case 'member': {
|
|
329
|
+
const member = item.data;
|
|
330
|
+
const role = member.channel_role || CHANNEL_ROLES.MEMBER;
|
|
331
|
+
const isTargetRemovable = canRemoveTargetMember(currentUserRole, role);
|
|
332
|
+
const canRemove = Boolean(isTargetRemovable && member.user_id !== currentUserId);
|
|
333
|
+
const canBan = Boolean(
|
|
334
|
+
canBanTargetMember(currentUserRole, role) && member.user_id !== currentUserId && !member.banned,
|
|
335
|
+
);
|
|
336
|
+
const canUnban = Boolean(
|
|
337
|
+
canBanTargetMember(currentUserRole, role) && member.user_id !== currentUserId && member.banned,
|
|
338
|
+
);
|
|
339
|
+
const canPromote = canPromoteTargetMember(currentUserRole, role) && member.user_id !== currentUserId;
|
|
340
|
+
const canDemote = canDemoteTargetMember(currentUserRole, role) && member.user_id !== currentUserId;
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<MemberItem
|
|
344
|
+
key={member?.user_id || index}
|
|
345
|
+
member={member}
|
|
346
|
+
AvatarComponent={AvatarComponent}
|
|
347
|
+
onRemove={onRemoveMember}
|
|
348
|
+
canRemove={canRemove}
|
|
349
|
+
onBan={onBanMember}
|
|
350
|
+
canBan={canBan}
|
|
351
|
+
onUnban={onUnbanMember}
|
|
352
|
+
canUnban={canUnban}
|
|
353
|
+
onPromote={onPromoteMember}
|
|
354
|
+
canPromote={canPromote}
|
|
355
|
+
onDemote={onDemoteMember}
|
|
356
|
+
canDemote={canDemote}
|
|
357
|
+
/>
|
|
358
|
+
);
|
|
292
359
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<MemberItem
|
|
318
|
-
key={member?.user_id || index}
|
|
319
|
-
member={member}
|
|
320
|
-
AvatarComponent={AvatarComponent}
|
|
321
|
-
onRemove={onRemoveMember}
|
|
322
|
-
canRemove={canRemove}
|
|
323
|
-
onBan={onBanMember}
|
|
324
|
-
canBan={canBan}
|
|
325
|
-
onUnban={onUnbanMember}
|
|
326
|
-
canUnban={canUnban}
|
|
327
|
-
onPromote={onPromoteMember}
|
|
328
|
-
canPromote={canPromote}
|
|
329
|
-
onDemote={onDemoteMember}
|
|
330
|
-
canDemote={canDemote}
|
|
331
|
-
/>
|
|
332
|
-
);
|
|
360
|
+
case 'media-row':
|
|
361
|
+
return (
|
|
362
|
+
<MediaRow
|
|
363
|
+
key={item.data[0]?.id || index}
|
|
364
|
+
row={item.data}
|
|
365
|
+
onClick={handleMediaClick}
|
|
366
|
+
MediaItemComponent={MediaItem}
|
|
367
|
+
/>
|
|
368
|
+
);
|
|
369
|
+
case 'link':
|
|
370
|
+
return <LinkItem key={item.data.id || index} item={item.data} />;
|
|
371
|
+
case 'file':
|
|
372
|
+
const fileItem = item.data as AttachmentItem;
|
|
373
|
+
return (
|
|
374
|
+
<FileItem
|
|
375
|
+
key={fileItem.id || index}
|
|
376
|
+
item={fileItem}
|
|
377
|
+
onClick={(url: string) => handleDownloadFile(url, fileItem.file_name)}
|
|
378
|
+
/>
|
|
379
|
+
);
|
|
380
|
+
default:
|
|
381
|
+
return null;
|
|
333
382
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
}, [
|
|
358
|
-
onAddMemberClick, AddMemberButtonComponent, addMemberButtonLabel,
|
|
359
|
-
currentUserRole, currentUserId, AvatarComponent, onRemoveMember, onBanMember, onUnbanMember, onPromoteMember, onDemoteMember,
|
|
360
|
-
handleMediaClick, MediaItem, handleDownloadFile,
|
|
361
|
-
MemberItem, LinkItem, FileItem
|
|
362
|
-
]);
|
|
383
|
+
},
|
|
384
|
+
[
|
|
385
|
+
onAddMemberClick,
|
|
386
|
+
AddMemberButtonComponent,
|
|
387
|
+
addMemberButtonLabel,
|
|
388
|
+
currentUserRole,
|
|
389
|
+
currentUserId,
|
|
390
|
+
AvatarComponent,
|
|
391
|
+
onRemoveMember,
|
|
392
|
+
onBanMember,
|
|
393
|
+
onUnbanMember,
|
|
394
|
+
onPromoteMember,
|
|
395
|
+
onDemoteMember,
|
|
396
|
+
handleMediaClick,
|
|
397
|
+
MediaItem,
|
|
398
|
+
handleDownloadFile,
|
|
399
|
+
MemberItem,
|
|
400
|
+
LinkItem,
|
|
401
|
+
FileItem,
|
|
402
|
+
],
|
|
403
|
+
);
|
|
363
404
|
|
|
364
405
|
const isTabEmpty = vlistData.length === 0 && !(loading && contentTab !== 'members');
|
|
365
406
|
const emptyLabel = contentTab === 'members' ? 'members' : contentTab;
|
|
@@ -89,8 +89,28 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
89
89
|
}, [defaultActions, hiddenActions]);
|
|
90
90
|
const ActionsComponent = ChannelActionsComponent || DefaultChannelActions;
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
const
|
|
92
|
+
// For DM channels, resolve name/image from the other member if channel.data.name is missing
|
|
93
|
+
const resolvedNameImage = useMemo(() => {
|
|
94
|
+
if (channel.data?.name) {
|
|
95
|
+
return { name: channel.data.name as string, image: channel.data.image as string | undefined };
|
|
96
|
+
}
|
|
97
|
+
// For DM (messaging) channels, find the other member's info
|
|
98
|
+
if (isDirectChannel(channel) && currentUserId && channel.state?.members) {
|
|
99
|
+
const members = Object.values(channel.state.members) as any[];
|
|
100
|
+
const other = members.find((m: any) => (m.user_id || m.user?.id) !== currentUserId);
|
|
101
|
+
if (other) {
|
|
102
|
+
const otherUser = other.user || other;
|
|
103
|
+
return {
|
|
104
|
+
name: otherUser.name || otherUser.id || channel.cid,
|
|
105
|
+
image: otherUser.image || otherUser.avatar || otherUser.avatar_url,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { name: channel.cid, image: channel.data?.image as string | undefined };
|
|
110
|
+
}, [channel.data?.name, channel.data?.image, channel.state?.members, currentUserId, channel.cid, updateCount]);
|
|
111
|
+
|
|
112
|
+
const name = resolvedNameImage.name;
|
|
113
|
+
const image = resolvedNameImage.image;
|
|
94
114
|
const showUnread = hasUnread && !isActive;
|
|
95
115
|
const avatarClassName = isGroupChannel(channel) ? 'ermis-avatar-wrapper--group' : undefined;
|
|
96
116
|
|
|
@@ -119,10 +139,15 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
119
139
|
return (
|
|
120
140
|
<div className={itemClass} onClick={handleClick}>
|
|
121
141
|
<div className="ermis-channel-list__item-avatar-wrapper">
|
|
122
|
-
<AvatarComponent image={image} name={name} size={
|
|
142
|
+
<AvatarComponent image={image} name={name} size={45} disableLightbox className={avatarClassName} />
|
|
123
143
|
{isOnline !== undefined && (
|
|
124
144
|
<span className={`ermis-channel-list__online-dot ermis-channel-list__online-dot--${isOnline ? 'online' : 'offline'}`} />
|
|
125
145
|
)}
|
|
146
|
+
{showUnread && unreadCount > 0 && (
|
|
147
|
+
<span className="ermis-channel-list__avatar-unread-badge">
|
|
148
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
126
151
|
</div>
|
|
127
152
|
<div className="ermis-channel-list__item-content">
|
|
128
153
|
<div className="ermis-channel-list__item-top-row">
|
|
@@ -261,6 +286,8 @@ type ChannelRowProps = {
|
|
|
261
286
|
videoMessageLabel?: React.ReactNode;
|
|
262
287
|
voiceRecordingMessageLabel?: React.ReactNode;
|
|
263
288
|
fileMessageLabel?: React.ReactNode;
|
|
289
|
+
encryptedMessageLabel?: React.ReactNode;
|
|
290
|
+
encryptedMessageUnavailableLabel?: React.ReactNode;
|
|
264
291
|
systemMessageTranslations?: SystemMessageTranslations;
|
|
265
292
|
signalMessageTranslations?: SignalMessageTranslations;
|
|
266
293
|
};
|
|
@@ -293,6 +320,8 @@ export const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
293
320
|
videoMessageLabel,
|
|
294
321
|
voiceRecordingMessageLabel,
|
|
295
322
|
fileMessageLabel,
|
|
323
|
+
encryptedMessageLabel,
|
|
324
|
+
encryptedMessageUnavailableLabel,
|
|
296
325
|
systemMessageTranslations,
|
|
297
326
|
signalMessageTranslations,
|
|
298
327
|
}) => {
|
|
@@ -320,6 +349,8 @@ export const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
320
349
|
videoMessageLabel,
|
|
321
350
|
voiceRecordingMessageLabel,
|
|
322
351
|
fileMessageLabel,
|
|
352
|
+
encryptedMessageLabel,
|
|
353
|
+
encryptedMessageUnavailableLabel,
|
|
323
354
|
systemMessageTranslations,
|
|
324
355
|
signalMessageTranslations,
|
|
325
356
|
}),
|
|
@@ -335,6 +366,8 @@ export const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
335
366
|
videoMessageLabel,
|
|
336
367
|
voiceRecordingMessageLabel,
|
|
337
368
|
fileMessageLabel,
|
|
369
|
+
encryptedMessageLabel,
|
|
370
|
+
encryptedMessageUnavailableLabel,
|
|
338
371
|
systemMessageTranslations,
|
|
339
372
|
signalMessageTranslations,
|
|
340
373
|
]
|
|
@@ -433,8 +466,11 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
433
466
|
videoMessageLabel,
|
|
434
467
|
voiceRecordingMessageLabel,
|
|
435
468
|
fileMessageLabel,
|
|
469
|
+
encryptedMessageLabel,
|
|
470
|
+
encryptedMessageUnavailableLabel,
|
|
436
471
|
systemMessageTranslations,
|
|
437
472
|
signalMessageTranslations,
|
|
473
|
+
showTopicPills = false,
|
|
438
474
|
}) => {
|
|
439
475
|
const { client, activeChannel, setActiveChannel } = useChatClient();
|
|
440
476
|
const { ChannelListErrorIndicator } = useChatComponents();
|
|
@@ -573,18 +609,32 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
573
609
|
onChannelSelect?.(channel);
|
|
574
610
|
|
|
575
611
|
// Mark as read when user selects a channel (skip if banned, blocked, or pending)
|
|
576
|
-
const
|
|
577
|
-
const
|
|
612
|
+
const activeCh = client.activeChannels[channel.cid] || channel;
|
|
613
|
+
const ms = activeCh.state?.membership as Record<string, unknown> | undefined;
|
|
614
|
+
const chState = activeCh.state as unknown as Record<string, unknown> | undefined;
|
|
578
615
|
const isBannedInChannel = Boolean(ms?.banned);
|
|
579
|
-
const isBlockedInChannel = isDirectChannel(
|
|
616
|
+
const isBlockedInChannel = isDirectChannel(activeCh) && Boolean(ms?.blocked);
|
|
580
617
|
const isPending = isPendingMember(ms?.channel_role as string);
|
|
581
618
|
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
582
619
|
|
|
583
|
-
if (!isBannedInChannel && !isBlockedInChannel && !isPending && !isSkipped
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
620
|
+
if (!isBannedInChannel && !isBlockedInChannel && !isPending && !isSkipped) {
|
|
621
|
+
let shouldUpdate = false;
|
|
622
|
+
if ((chState?.unreadCount as number) > 0) {
|
|
623
|
+
activeCh.markRead().catch(() => { });
|
|
624
|
+
// Optimistically reset unread to update UI immediately
|
|
625
|
+
if (chState) chState.unreadCount = 0;
|
|
626
|
+
shouldUpdate = true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Also optimistic update on the stale channel just in case
|
|
630
|
+
if (channel.state && (channel.state as any).unreadCount > 0) {
|
|
631
|
+
(channel.state as any).unreadCount = 0;
|
|
632
|
+
shouldUpdate = true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (shouldUpdate) {
|
|
636
|
+
setChannels((prev) => [...prev]);
|
|
637
|
+
}
|
|
588
638
|
}
|
|
589
639
|
},
|
|
590
640
|
[setActiveChannel, onChannelSelect, setChannels],
|
|
@@ -649,6 +699,8 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
649
699
|
videoMessageLabel={videoMessageLabel}
|
|
650
700
|
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
651
701
|
fileMessageLabel={fileMessageLabel}
|
|
702
|
+
encryptedMessageLabel={encryptedMessageLabel}
|
|
703
|
+
encryptedMessageUnavailableLabel={encryptedMessageUnavailableLabel}
|
|
652
704
|
systemMessageTranslations={systemMessageTranslations}
|
|
653
705
|
signalMessageTranslations={signalMessageTranslations}
|
|
654
706
|
/>
|
|
@@ -660,7 +712,8 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
660
712
|
</div>
|
|
661
713
|
)} */}
|
|
662
714
|
{regularChannels.map((channel: Channel) => {
|
|
663
|
-
const isActive = activeChannel?.cid === channel.cid
|
|
715
|
+
const isActive = activeChannel?.cid === channel.cid ||
|
|
716
|
+
(activeChannel?.data?.parent_cid === channel.cid);
|
|
664
717
|
const isTeamWithTopics = hasTopicsEnabled(channel);
|
|
665
718
|
|
|
666
719
|
if (isTeamWithTopics) {
|
|
@@ -671,7 +724,10 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
671
724
|
key={channel.cid}
|
|
672
725
|
channel={channel}
|
|
673
726
|
isActive={isActive}
|
|
674
|
-
onDrillDown={
|
|
727
|
+
onDrillDown={(c) => {
|
|
728
|
+
handleSelect(c);
|
|
729
|
+
if (onTopicDrillDown) onTopicDrillDown(c);
|
|
730
|
+
}}
|
|
675
731
|
AvatarComponent={AvatarComponent}
|
|
676
732
|
maxVisibleTopics={maxVisibleTopics}
|
|
677
733
|
moreTopicsLabel={moreTopicsLabel}
|
|
@@ -692,6 +748,7 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
692
748
|
fileMessageLabel={fileMessageLabel}
|
|
693
749
|
systemMessageTranslations={systemMessageTranslations}
|
|
694
750
|
signalMessageTranslations={signalMessageTranslations}
|
|
751
|
+
showTopicPills={showTopicPills}
|
|
695
752
|
/>
|
|
696
753
|
);
|
|
697
754
|
}
|
|
@@ -726,6 +783,8 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
726
783
|
videoMessageLabel={videoMessageLabel}
|
|
727
784
|
voiceRecordingMessageLabel={voiceRecordingMessageLabel}
|
|
728
785
|
fileMessageLabel={fileMessageLabel}
|
|
786
|
+
encryptedMessageLabel={encryptedMessageLabel}
|
|
787
|
+
encryptedMessageUnavailableLabel={encryptedMessageUnavailableLabel}
|
|
729
788
|
systemMessageTranslations={systemMessageTranslations}
|
|
730
789
|
signalMessageTranslations={signalMessageTranslations}
|
|
731
790
|
/>
|