@ermis-network/ermis-chat-react 1.0.9 → 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 +8320 -3427
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1277 -291
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +1131 -99
- package/dist/index.d.ts +1131 -99
- package/dist/index.mjs +8168 -3319
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/channelTypeUtils.ts +1 -1
- package/src/components/Avatar.tsx +2 -1
- package/src/components/Channel.tsx +6 -5
- package/src/components/ChannelActions.tsx +67 -3
- package/src/components/ChannelHeader.tsx +27 -37
- package/src/components/ChannelInfo/AddMemberModal.tsx +12 -2
- package/src/components/ChannelInfo/ChannelInfo.tsx +410 -187
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
- package/src/components/ChannelInfo/EditChannelModal.tsx +6 -3
- package/src/components/ChannelInfo/MediaGridItem.tsx +215 -68
- package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
- package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
- package/src/components/ChannelInfo/States.tsx +1 -1
- package/src/components/ChannelInfo/index.ts +3 -0
- package/src/components/ChannelInfo/useChannelInfoTabs.tsx +427 -0
- package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
- package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
- package/src/components/ChannelList.tsx +247 -301
- package/src/components/CreateChannelModal.tsx +290 -93
- package/src/components/Dropdown.tsx +1 -16
- package/src/components/EditPreview.tsx +1 -0
- package/src/components/ErmisCallProvider.tsx +72 -17
- package/src/components/ErmisCallUI.tsx +43 -20
- package/src/components/FilesPreview.tsx +8 -12
- package/src/components/FlatTopicGroupItem.tsx +243 -0
- package/src/components/ForwardMessageModal.tsx +43 -81
- package/src/components/MediaLightbox.tsx +454 -292
- package/src/components/MentionSuggestions.tsx +47 -35
- package/src/components/MessageActionsBox.tsx +6 -1
- package/src/components/MessageInput.tsx +165 -17
- package/src/components/MessageInputDefaults.tsx +127 -1
- package/src/components/MessageItem.tsx +155 -43
- package/src/components/MessageQuickReactions.tsx +153 -23
- package/src/components/MessageReactions.tsx +49 -3
- package/src/components/MessageRenderers.tsx +1114 -445
- package/src/components/Panel.tsx +1 -14
- package/src/components/PinnedMessages.tsx +55 -15
- package/src/components/PreviewOverlay.tsx +24 -0
- package/src/components/QuotedMessagePreview.tsx +99 -8
- package/src/components/ReadReceipts.tsx +2 -1
- package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
- package/src/components/RecoveryPin/index.ts +19 -0
- package/src/components/TopicList.tsx +236 -0
- package/src/components/TopicModal.tsx +4 -1
- package/src/components/TypingIndicator.tsx +17 -8
- package/src/components/UserPicker.tsx +94 -16
- package/src/components/VirtualMessageList.tsx +419 -113
- package/src/context/ChatComponentsContext.tsx +14 -0
- package/src/context/ChatProvider.tsx +44 -14
- package/src/context/ErmisCallContext.tsx +4 -0
- package/src/hooks/useChannelCapabilities.ts +7 -4
- package/src/hooks/useChannelData.ts +10 -3
- package/src/hooks/useChannelListUpdates.ts +94 -21
- package/src/hooks/useChannelMessages.ts +391 -42
- package/src/hooks/useChannelRowUpdates.ts +36 -5
- package/src/hooks/useChatUser.ts +39 -0
- package/src/hooks/useContactChannels.ts +45 -0
- package/src/hooks/useContactCount.ts +50 -0
- package/src/hooks/useDownloadHandler.ts +36 -0
- package/src/hooks/useDragAndDrop.ts +79 -0
- 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 +309 -0
- package/src/hooks/useInviteChannels.ts +88 -0
- package/src/hooks/useInviteCount.ts +104 -0
- package/src/hooks/useLoadMessages.ts +16 -4
- package/src/hooks/useMentions.ts +60 -7
- package/src/hooks/useMessageActions.ts +19 -10
- package/src/hooks/useMessageSend.ts +64 -12
- package/src/hooks/usePendingE2eeSends.ts +29 -0
- package/src/hooks/usePendingState.ts +21 -4
- package/src/hooks/usePreviewState.ts +69 -0
- package/src/hooks/useRecoveryPin.ts +287 -0
- package/src/hooks/useScrollToMessage.ts +29 -4
- package/src/hooks/useStickerPicker.ts +62 -0
- package/src/hooks/useTopicGroupUpdates.ts +235 -0
- package/src/index.ts +79 -6
- package/src/messageTypeUtils.ts +27 -1
- package/src/styles/_base.css +0 -1
- package/src/styles/_call-ui.css +59 -2
- package/src/styles/_channel-info.css +50 -4
- package/src/styles/_channel-list.css +131 -68
- package/src/styles/_create-channel-modal.css +10 -0
- package/src/styles/_forward-modal.css +16 -1
- package/src/styles/_media-lightbox.css +67 -2
- package/src/styles/_mentions.css +1 -1
- package/src/styles/_message-actions.css +3 -4
- package/src/styles/_message-bubble.css +631 -112
- package/src/styles/_message-input.css +139 -0
- package/src/styles/_message-list.css +91 -18
- package/src/styles/_message-quick-reactions.css +105 -32
- package/src/styles/_message-reactions.css +22 -32
- package/src/styles/_modal.css +2 -1
- package/src/styles/_preview-overlay.css +38 -0
- package/src/styles/_recovery-pin.css +97 -0
- package/src/styles/_tokens.css +22 -20
- package/src/styles/_typing-indicator.css +26 -10
- package/src/styles/index.css +2 -0
- package/src/types.ts +477 -15
- package/src/utils/avatarColors.ts +48 -0
- package/src/utils.ts +219 -16
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import type { ModalProps, DropdownProps, PanelProps, ForwardMessageModalProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export type ChatComponentsContextValue = {
|
|
5
|
+
ModalComponent?: React.ComponentType<ModalProps>;
|
|
6
|
+
DropdownComponent?: React.ComponentType<DropdownProps>;
|
|
7
|
+
PanelComponent?: React.ComponentType<PanelProps>;
|
|
8
|
+
ForwardMessageModalComponent?: React.ComponentType<ForwardMessageModalProps>;
|
|
9
|
+
ChannelListErrorIndicator?: React.ComponentType<{ text?: string; onRetry?: () => void }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ChatComponentsContext = createContext<ChatComponentsContextValue>({});
|
|
13
|
+
|
|
14
|
+
export const useChatComponents = () => useContext(ChatComponentsContext);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { createContext, useState, useCallback } from 'react';
|
|
1
|
+
import React, { createContext, useState, useCallback, useRef, useMemo } from 'react';
|
|
2
2
|
import type { Channel, FormatMessageResponse } from '@ermis-network/ermis-chat-sdk';
|
|
3
3
|
import type { Theme, ChatContextValue, ChatProviderProps, ReadStateEntry } from '../types';
|
|
4
4
|
import { ErmisCallProvider } from '../components/ErmisCallProvider';
|
|
5
5
|
import { ErmisCallUI } from '../components/ErmisCallUI';
|
|
6
|
+
import { ChatComponentsContext } from './ChatComponentsContext';
|
|
6
7
|
|
|
7
8
|
export type { Theme, ChatContextValue, ChatProviderProps } from '../types';
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ export const ChatContext = createContext<ChatContextValue | null>(null);
|
|
|
11
12
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
12
13
|
client,
|
|
13
14
|
children,
|
|
15
|
+
components = {},
|
|
14
16
|
initialTheme = 'light',
|
|
15
17
|
enableCall = false,
|
|
16
18
|
callSessionId,
|
|
@@ -36,18 +38,22 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
36
38
|
const [jumpToMessageId, setJumpToMessageId] = useState<string | null>(null);
|
|
37
39
|
|
|
38
40
|
const activeChannel = activeChannelRaw;
|
|
41
|
+
const activeChannelCidRef = useRef<string | null>(null);
|
|
42
|
+
|
|
43
|
+
// In-memory draft storage — Map<cid, { html: string; files: any[] }>
|
|
44
|
+
// O(1) lookup/insert/delete, bounded by number of visited channels per session
|
|
45
|
+
const draftsRef = useRef<Map<string, { html: string; files: any[] }>>(new Map());
|
|
39
46
|
|
|
40
47
|
const setActiveChannel = useCallback((channel: Channel | null) => {
|
|
48
|
+
const newCid = channel?.cid || null;
|
|
49
|
+
if (activeChannelCidRef.current === newCid) return;
|
|
50
|
+
|
|
51
|
+
activeChannelCidRef.current = newCid;
|
|
41
52
|
setActiveChannelRaw(channel);
|
|
42
53
|
setQuotedMessage(null);
|
|
43
54
|
setEditingMessage(null);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
setReadState({ ...channel.state.read });
|
|
47
|
-
} else {
|
|
48
|
-
setMessages([]);
|
|
49
|
-
setReadState({});
|
|
50
|
-
}
|
|
55
|
+
setMessages([]);
|
|
56
|
+
setReadState({});
|
|
51
57
|
}, []);
|
|
52
58
|
|
|
53
59
|
/** Re-read messages from SDK state into React state */
|
|
@@ -57,6 +63,25 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
57
63
|
}
|
|
58
64
|
}, [activeChannel]);
|
|
59
65
|
|
|
66
|
+
/** Save a draft message (innerHTML and files) for a specific channel */
|
|
67
|
+
const setDraft = useCallback((cid: string, draft: { html: string; files: any[] }) => {
|
|
68
|
+
if ((draft.html && draft.html.trim()) || (draft.files && draft.files.length > 0)) {
|
|
69
|
+
draftsRef.current.set(cid, draft);
|
|
70
|
+
} else {
|
|
71
|
+
draftsRef.current.delete(cid);
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
/** Retrieve the saved draft for a specific channel */
|
|
76
|
+
const getDraft = useCallback((cid: string): { html: string; files: any[] } | undefined => {
|
|
77
|
+
return draftsRef.current.get(cid);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
/** Clear all saved drafts (e.g. on logout) */
|
|
81
|
+
const clearAllDrafts = useCallback(() => {
|
|
82
|
+
draftsRef.current.clear();
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
60
85
|
const value: ChatContextValue = {
|
|
61
86
|
client,
|
|
62
87
|
activeChannel,
|
|
@@ -77,6 +102,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
77
102
|
jumpToMessageId,
|
|
78
103
|
setJumpToMessageId,
|
|
79
104
|
enableCall,
|
|
105
|
+
setDraft,
|
|
106
|
+
getDraft,
|
|
107
|
+
clearAllDrafts,
|
|
80
108
|
};
|
|
81
109
|
|
|
82
110
|
const CallUIView = CallUIComponent ? <CallUIComponent /> : (
|
|
@@ -87,12 +115,14 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
87
115
|
);
|
|
88
116
|
|
|
89
117
|
const content = (
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
<ChatComponentsContext.Provider value={components}>
|
|
119
|
+
<ChatContext.Provider value={value}>
|
|
120
|
+
<div className={`ermis-chat ermis-chat--${theme}`}>
|
|
121
|
+
{children}
|
|
122
|
+
{enableCall && CallUIView}
|
|
123
|
+
</div>
|
|
124
|
+
</ChatContext.Provider>
|
|
125
|
+
</ChatComponentsContext.Provider>
|
|
96
126
|
);
|
|
97
127
|
|
|
98
128
|
if (enableCall) {
|
|
@@ -32,6 +32,10 @@ export type CallContextValue = {
|
|
|
32
32
|
isRemoteVideoMuted: boolean;
|
|
33
33
|
upgradeCall: () => Promise<void>;
|
|
34
34
|
callDuration: number;
|
|
35
|
+
isAccepting: boolean;
|
|
36
|
+
isRejecting: boolean;
|
|
37
|
+
isEnding: boolean;
|
|
38
|
+
resetCall: () => void;
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
export const ErmisCallContext = React.createContext<CallContextValue | undefined>(undefined);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useChatClient } from './useChatClient';
|
|
3
|
+
import { usePreviewState } from './usePreviewState';
|
|
3
4
|
import { isGroupChannel } from '../channelTypeUtils';
|
|
4
5
|
import { canManageChannel, CHANNEL_ROLES } from '../channelRoleUtils';
|
|
5
6
|
|
|
@@ -19,18 +20,20 @@ export const useChannelCapabilities = () => {
|
|
|
19
20
|
}, [activeChannel]);
|
|
20
21
|
|
|
21
22
|
const currentUserId = client?.userID || '';
|
|
23
|
+
const { isPreviewMode } = usePreviewState(activeChannel, currentUserId);
|
|
22
24
|
const isGroupCh = isGroupChannel(activeChannel);
|
|
23
25
|
const role = (activeChannel?.state as any)?.members?.[currentUserId]?.channel_role;
|
|
24
26
|
|
|
25
|
-
const isOwner = role === CHANNEL_ROLES.OWNER || activeChannel?.data?.created_by_id === currentUserId;
|
|
26
|
-
const isModerator = role === CHANNEL_ROLES.MODERATOR;
|
|
27
|
-
const isOwnerOrModerator = isOwner || isModerator || canManageChannel(role);
|
|
27
|
+
const isOwner = isPreviewMode ? false : (role === CHANNEL_ROLES.OWNER || activeChannel?.data?.created_by_id === currentUserId);
|
|
28
|
+
const isModerator = isPreviewMode ? false : (role === CHANNEL_ROLES.MODERATOR);
|
|
29
|
+
const isOwnerOrModerator = isOwner || isModerator || (!isPreviewMode && canManageChannel(role));
|
|
28
30
|
|
|
29
31
|
const capabilities: string[] = isGroupCh ? (activeChannel?.data as any)?.member_capabilities || [] : [];
|
|
30
32
|
|
|
31
33
|
const hasCapability = useCallback((cap: string) => {
|
|
34
|
+
if (isPreviewMode) return false;
|
|
32
35
|
return !isGroupCh || isOwnerOrModerator || capabilities.includes(cap);
|
|
33
|
-
}, [isGroupCh, isOwnerOrModerator, capabilities, updateTick]);
|
|
36
|
+
}, [isGroupCh, isOwnerOrModerator, capabilities, updateTick, isPreviewMode]);
|
|
34
37
|
|
|
35
38
|
return {
|
|
36
39
|
isGroupChannel: isGroupCh,
|
|
@@ -43,13 +43,20 @@ export const useChannelProfile = (channel: Channel | null | undefined) => {
|
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
if (!channel) return;
|
|
45
45
|
const updateChannel = () => setChannelUpdateCount(c => c + 1);
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const sub1 = channel.on('channel.updated', updateChannel);
|
|
47
|
+
const sub2 = channel.on('channel.pinned', updateChannel);
|
|
48
|
+
const sub3 = channel.on('channel.unpinned', updateChannel);
|
|
49
|
+
return () => {
|
|
50
|
+
sub1.unsubscribe();
|
|
51
|
+
sub2.unsubscribe();
|
|
52
|
+
sub3.unsubscribe();
|
|
53
|
+
};
|
|
48
54
|
}, [channel]);
|
|
49
55
|
|
|
50
56
|
const channelName = useMemo(() => channel?.data?.name || channel?.cid || 'Unknown Channel', [channel?.data?.name, channel?.cid, channel?.type, channelUpdateCount]);
|
|
51
57
|
const channelImage = useMemo(() => channel?.data?.image as string | undefined, [channel?.data?.image, channelUpdateCount]);
|
|
52
58
|
const channelDescription = useMemo(() => channel?.data?.description as string | undefined, [channel?.data?.description, channelUpdateCount]);
|
|
59
|
+
const isPinned = useMemo(() => channel?.data?.is_pinned === true, [channel?.data?.is_pinned, channelUpdateCount]);
|
|
53
60
|
|
|
54
|
-
return { channelName, channelImage, channelDescription };
|
|
61
|
+
return { channelName, channelImage, channelDescription, isPinned };
|
|
55
62
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import type { Channel, Event } from '@ermis-network/ermis-chat-sdk';
|
|
3
3
|
import { useChatClient } from './useChatClient';
|
|
4
|
-
import { isDirectChannel } from '../channelTypeUtils';
|
|
4
|
+
import { isDirectChannel, isGroupChannel } from '../channelTypeUtils';
|
|
5
5
|
import { isPendingMember } from '../channelRoleUtils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -18,6 +18,7 @@ import { isPendingMember } from '../channelRoleUtils';
|
|
|
18
18
|
export function useChannelListUpdates(
|
|
19
19
|
channels: Channel[],
|
|
20
20
|
setChannels: React.Dispatch<React.SetStateAction<Channel[]>>,
|
|
21
|
+
onOwnMessageNew?: () => void,
|
|
21
22
|
): void {
|
|
22
23
|
const { client, activeChannel, setActiveChannel } = useChatClient();
|
|
23
24
|
|
|
@@ -25,17 +26,23 @@ export function useChannelListUpdates(
|
|
|
25
26
|
const activeChannelRef = useRef(activeChannel);
|
|
26
27
|
activeChannelRef.current = activeChannel;
|
|
27
28
|
|
|
29
|
+
// Ref to always have the latest callback without re-subscribing
|
|
30
|
+
const onOwnMessageNewRef = useRef(onOwnMessageNew);
|
|
31
|
+
onOwnMessageNewRef.current = onOwnMessageNew;
|
|
32
|
+
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
// --- message.new: re-sort + auto mark-read ---
|
|
30
35
|
const handleNewMessage = (event: Event) => {
|
|
31
36
|
const eventCid = event.cid;
|
|
32
37
|
if (!eventCid) return;
|
|
33
38
|
|
|
39
|
+
const isOwnMessage = event.user?.id === client.userID;
|
|
40
|
+
|
|
34
41
|
// If the new message is on the active channel and from someone else,
|
|
35
42
|
// mark it as read immediately so unreadCount resets to 0.
|
|
36
43
|
// Skip markRead if the current user is banned, blocked, or pending in that channel.
|
|
37
44
|
const active = activeChannelRef.current;
|
|
38
|
-
if (active?.cid === eventCid &&
|
|
45
|
+
if (active?.cid === eventCid && !isOwnMessage) {
|
|
39
46
|
const isBannedInActive = Boolean(active.state?.membership?.banned);
|
|
40
47
|
const isBlockedInActive = isDirectChannel(active) && Boolean(active.state?.membership?.blocked);
|
|
41
48
|
const isPendingActive = isPendingMember(active.state?.membership?.channel_role as string);
|
|
@@ -48,13 +55,23 @@ export function useChannelListUpdates(
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
setChannels((prev) => {
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
let targetIdx = prev.findIndex((ch) => ch.cid === eventCid);
|
|
59
|
+
|
|
60
|
+
// If not found directly, check if this is a topic message — move the parent channel to top
|
|
61
|
+
if (targetIdx < 0) {
|
|
62
|
+
const topicChannel = client.activeChannels[eventCid];
|
|
63
|
+
const parentCid = topicChannel?.data?.parent_cid as string | undefined;
|
|
64
|
+
if (parentCid) {
|
|
65
|
+
targetIdx = prev.findIndex((ch) => ch.cid === parentCid);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (targetIdx <= 0) {
|
|
53
70
|
// Already at top or not found — just create a new reference
|
|
54
|
-
return
|
|
71
|
+
return targetIdx === 0 ? [...prev] : prev;
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
const channel = prev[
|
|
74
|
+
const channel = prev[targetIdx];
|
|
58
75
|
|
|
59
76
|
// Don't move banned channels to the top
|
|
60
77
|
if (channel.state?.membership?.banned) {
|
|
@@ -63,10 +80,17 @@ export function useChannelListUpdates(
|
|
|
63
80
|
|
|
64
81
|
// Move channel to the top
|
|
65
82
|
const updated = [...prev];
|
|
66
|
-
const [ch] = updated.splice(
|
|
83
|
+
const [ch] = updated.splice(targetIdx, 1);
|
|
67
84
|
updated.unshift(ch);
|
|
68
85
|
return updated;
|
|
69
86
|
});
|
|
87
|
+
|
|
88
|
+
// Notify the component layer that the current user sent a message
|
|
89
|
+
// so it can scroll to top. Use setTimeout(0) to ensure React has
|
|
90
|
+
// flushed the setChannels state update before the scroll fires.
|
|
91
|
+
if (isOwnMessage && onOwnMessageNewRef.current) {
|
|
92
|
+
setTimeout(() => onOwnMessageNewRef.current?.(), 0);
|
|
93
|
+
}
|
|
70
94
|
};
|
|
71
95
|
|
|
72
96
|
// --- channel.deleted: remove from list and reset active ---
|
|
@@ -178,32 +202,74 @@ export function useChannelListUpdates(
|
|
|
178
202
|
}
|
|
179
203
|
};
|
|
180
204
|
|
|
181
|
-
// --- notification.invite_accepted: force re-grouping ---
|
|
182
|
-
const handleMemberUpdated = (event: Event) => {
|
|
205
|
+
// --- notification.invite_accepted / member.joined: force re-grouping or add to list ---
|
|
206
|
+
const handleMemberUpdated = async (event: Event) => {
|
|
183
207
|
const updatedUserId = event.member?.user_id || event.member?.user?.id || event.user?.id;
|
|
184
208
|
if (updatedUserId === client.userID) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
event.
|
|
190
|
-
|
|
191
|
-
? `${(event as Record<string, unknown>).channel_type}:${(event as Record<string, unknown>).channel_id}`
|
|
192
|
-
: undefined);
|
|
209
|
+
const eventCid =
|
|
210
|
+
event.cid ||
|
|
211
|
+
event.channel?.cid ||
|
|
212
|
+
((event as Record<string, unknown>).channel_id
|
|
213
|
+
? `${(event as Record<string, unknown>).channel_type}:${(event as Record<string, unknown>).channel_id}`
|
|
214
|
+
: undefined);
|
|
193
215
|
|
|
216
|
+
setChannels((prev) => {
|
|
194
217
|
if (eventCid && event.member) {
|
|
195
218
|
const targetChannel = prev.find((c) => c.cid === eventCid);
|
|
196
|
-
// We forcefully map the updated incoming member data into the static channel representation
|
|
197
219
|
if (targetChannel && targetChannel.state) {
|
|
220
|
+
// Channel already in list — just update membership for re-grouping
|
|
198
221
|
targetChannel.state.membership = {
|
|
199
222
|
...targetChannel.state.membership,
|
|
200
223
|
...event.member,
|
|
201
224
|
} as unknown as Record<string, unknown>;
|
|
202
225
|
}
|
|
203
226
|
}
|
|
204
|
-
|
|
205
|
-
return [...prev]; // Force react map to regenerate
|
|
227
|
+
return [...prev];
|
|
206
228
|
});
|
|
229
|
+
|
|
230
|
+
// For team channels with topics: re-watch to load topics from server.
|
|
231
|
+
// When the user was pending, queryChannels did not return topics.
|
|
232
|
+
// After accepting the invite, we need a fresh query to hydrate them.
|
|
233
|
+
if (eventCid) {
|
|
234
|
+
const existingChannel = client.activeChannels[eventCid];
|
|
235
|
+
if (existingChannel && isGroupChannel(existingChannel) && existingChannel.data?.topics_enabled) {
|
|
236
|
+
existingChannel.watch().then(() => {
|
|
237
|
+
// Notify React hooks (useTopicGroupUpdates) that topics have been loaded
|
|
238
|
+
existingChannel._callChannelListeners({
|
|
239
|
+
type: 'channel.updated',
|
|
240
|
+
cid: existingChannel.cid,
|
|
241
|
+
channel: existingChannel.data,
|
|
242
|
+
} as any);
|
|
243
|
+
// Also trigger channel list re-render
|
|
244
|
+
setChannels((p) => [...p]);
|
|
245
|
+
}).catch((err) => {
|
|
246
|
+
console.error('Failed to re-watch team channel after invite accepted:', err);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If the channel is NOT in the list yet (e.g. user just joined a public channel
|
|
252
|
+
// from search), add it — same logic as handleChannelCreated
|
|
253
|
+
if (eventCid) {
|
|
254
|
+
setChannels((prev) => {
|
|
255
|
+
if (prev.some((c) => c.cid === eventCid)) return prev; // already in list
|
|
256
|
+
const type = event.channel?.type || (event as Record<string, unknown>).channel_type;
|
|
257
|
+
const id = event.channel?.id || (event as Record<string, unknown>).channel_id;
|
|
258
|
+
if (!type || !id) return prev;
|
|
259
|
+
const channelInstance = client.channel(type as string, id as string);
|
|
260
|
+
if (channelInstance.state) {
|
|
261
|
+
channelInstance.state.membership = {
|
|
262
|
+
...channelInstance.state.membership,
|
|
263
|
+
...event.member,
|
|
264
|
+
} as unknown as Record<string, unknown>;
|
|
265
|
+
}
|
|
266
|
+
// Watch if not initialized so we get full state
|
|
267
|
+
if (!channelInstance.initialized) {
|
|
268
|
+
channelInstance.watch().catch(() => {});
|
|
269
|
+
}
|
|
270
|
+
return [channelInstance, ...prev];
|
|
271
|
+
});
|
|
272
|
+
}
|
|
207
273
|
}
|
|
208
274
|
};
|
|
209
275
|
|
|
@@ -215,7 +281,10 @@ export function useChannelListUpdates(
|
|
|
215
281
|
const sub1 = client.on('message.new', handleNewMessage);
|
|
216
282
|
const sub2 = client.on('channel.deleted', handleChannelDeleted);
|
|
217
283
|
const sub3 = client.on('member.removed', handleMemberRemoved);
|
|
218
|
-
const sub4 = client.on('channel.created', (event) =>
|
|
284
|
+
const sub4 = client.on('channel.created', (event) => {
|
|
285
|
+
const isCreator = event.user?.id === client.userID || event.user_id === client.userID;
|
|
286
|
+
handleChannelCreated(event, !isCreator);
|
|
287
|
+
});
|
|
219
288
|
const sub5 = client.on('member.added', handleMemberAdded);
|
|
220
289
|
const sub6 = client.on('notification.added_to_channel', handleMemberAdded);
|
|
221
290
|
const sub7 = client.on('notification.invite_rejected', handleMemberRemoved);
|
|
@@ -226,6 +295,9 @@ export function useChannelListUpdates(
|
|
|
226
295
|
const sub12 = client.on('channel.pinned', handleGenericUpdate);
|
|
227
296
|
const sub13 = client.on('channel.unpinned', handleGenericUpdate);
|
|
228
297
|
const sub14 = client.on('notification.invite_messaging_skipped', handleMemberUpdated);
|
|
298
|
+
// When a user joins a public channel (action='join'), the server sends member.joined
|
|
299
|
+
// instead of notification.invite_accepted — handle it to re-group the channel list
|
|
300
|
+
const sub15 = client.on('member.joined', handleMemberUpdated);
|
|
229
301
|
|
|
230
302
|
return () => {
|
|
231
303
|
sub1.unsubscribe();
|
|
@@ -242,6 +314,7 @@ export function useChannelListUpdates(
|
|
|
242
314
|
sub12.unsubscribe();
|
|
243
315
|
sub13.unsubscribe();
|
|
244
316
|
sub14.unsubscribe();
|
|
317
|
+
sub15.unsubscribe();
|
|
245
318
|
};
|
|
246
319
|
}, [client, setChannels, setActiveChannel]);
|
|
247
320
|
}
|