@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.
Files changed (113) hide show
  1. package/README.md +144 -0
  2. package/dist/index.cjs +8320 -3427
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1277 -291
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.mts +1131 -99
  7. package/dist/index.d.ts +1131 -99
  8. package/dist/index.mjs +8168 -3319
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +9 -4
  11. package/src/channelTypeUtils.ts +1 -1
  12. package/src/components/Avatar.tsx +2 -1
  13. package/src/components/Channel.tsx +6 -5
  14. package/src/components/ChannelActions.tsx +67 -3
  15. package/src/components/ChannelHeader.tsx +27 -37
  16. package/src/components/ChannelInfo/AddMemberModal.tsx +12 -2
  17. package/src/components/ChannelInfo/ChannelInfo.tsx +410 -187
  18. package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
  19. package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
  20. package/src/components/ChannelInfo/EditChannelModal.tsx +6 -3
  21. package/src/components/ChannelInfo/MediaGridItem.tsx +215 -68
  22. package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
  23. package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
  24. package/src/components/ChannelInfo/States.tsx +1 -1
  25. package/src/components/ChannelInfo/index.ts +3 -0
  26. package/src/components/ChannelInfo/useChannelInfoTabs.tsx +427 -0
  27. package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
  28. package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
  29. package/src/components/ChannelList.tsx +247 -301
  30. package/src/components/CreateChannelModal.tsx +290 -93
  31. package/src/components/Dropdown.tsx +1 -16
  32. package/src/components/EditPreview.tsx +1 -0
  33. package/src/components/ErmisCallProvider.tsx +72 -17
  34. package/src/components/ErmisCallUI.tsx +43 -20
  35. package/src/components/FilesPreview.tsx +8 -12
  36. package/src/components/FlatTopicGroupItem.tsx +243 -0
  37. package/src/components/ForwardMessageModal.tsx +43 -81
  38. package/src/components/MediaLightbox.tsx +454 -292
  39. package/src/components/MentionSuggestions.tsx +47 -35
  40. package/src/components/MessageActionsBox.tsx +6 -1
  41. package/src/components/MessageInput.tsx +165 -17
  42. package/src/components/MessageInputDefaults.tsx +127 -1
  43. package/src/components/MessageItem.tsx +155 -43
  44. package/src/components/MessageQuickReactions.tsx +153 -23
  45. package/src/components/MessageReactions.tsx +49 -3
  46. package/src/components/MessageRenderers.tsx +1114 -445
  47. package/src/components/Panel.tsx +1 -14
  48. package/src/components/PinnedMessages.tsx +55 -15
  49. package/src/components/PreviewOverlay.tsx +24 -0
  50. package/src/components/QuotedMessagePreview.tsx +99 -8
  51. package/src/components/ReadReceipts.tsx +2 -1
  52. package/src/components/RecoveryPin/RecoveryPin.tsx +279 -0
  53. package/src/components/RecoveryPin/index.ts +19 -0
  54. package/src/components/TopicList.tsx +236 -0
  55. package/src/components/TopicModal.tsx +4 -1
  56. package/src/components/TypingIndicator.tsx +17 -8
  57. package/src/components/UserPicker.tsx +94 -16
  58. package/src/components/VirtualMessageList.tsx +419 -113
  59. package/src/context/ChatComponentsContext.tsx +14 -0
  60. package/src/context/ChatProvider.tsx +44 -14
  61. package/src/context/ErmisCallContext.tsx +4 -0
  62. package/src/hooks/useChannelCapabilities.ts +7 -4
  63. package/src/hooks/useChannelData.ts +10 -3
  64. package/src/hooks/useChannelListUpdates.ts +94 -21
  65. package/src/hooks/useChannelMessages.ts +391 -42
  66. package/src/hooks/useChannelRowUpdates.ts +36 -5
  67. package/src/hooks/useChatUser.ts +39 -0
  68. package/src/hooks/useContactChannels.ts +45 -0
  69. package/src/hooks/useContactCount.ts +50 -0
  70. package/src/hooks/useDownloadHandler.ts +36 -0
  71. package/src/hooks/useDragAndDrop.ts +79 -0
  72. package/src/hooks/useE2eeAttachmentRenderer.ts +204 -0
  73. package/src/hooks/useE2eeFileUpload.ts +38 -0
  74. package/src/hooks/useFileUpload.ts +25 -5
  75. package/src/hooks/useForwardMessage.ts +309 -0
  76. package/src/hooks/useInviteChannels.ts +88 -0
  77. package/src/hooks/useInviteCount.ts +104 -0
  78. package/src/hooks/useLoadMessages.ts +16 -4
  79. package/src/hooks/useMentions.ts +60 -7
  80. package/src/hooks/useMessageActions.ts +19 -10
  81. package/src/hooks/useMessageSend.ts +64 -12
  82. package/src/hooks/usePendingE2eeSends.ts +29 -0
  83. package/src/hooks/usePendingState.ts +21 -4
  84. package/src/hooks/usePreviewState.ts +69 -0
  85. package/src/hooks/useRecoveryPin.ts +287 -0
  86. package/src/hooks/useScrollToMessage.ts +29 -4
  87. package/src/hooks/useStickerPicker.ts +62 -0
  88. package/src/hooks/useTopicGroupUpdates.ts +235 -0
  89. package/src/index.ts +79 -6
  90. package/src/messageTypeUtils.ts +27 -1
  91. package/src/styles/_base.css +0 -1
  92. package/src/styles/_call-ui.css +59 -2
  93. package/src/styles/_channel-info.css +50 -4
  94. package/src/styles/_channel-list.css +131 -68
  95. package/src/styles/_create-channel-modal.css +10 -0
  96. package/src/styles/_forward-modal.css +16 -1
  97. package/src/styles/_media-lightbox.css +67 -2
  98. package/src/styles/_mentions.css +1 -1
  99. package/src/styles/_message-actions.css +3 -4
  100. package/src/styles/_message-bubble.css +631 -112
  101. package/src/styles/_message-input.css +139 -0
  102. package/src/styles/_message-list.css +91 -18
  103. package/src/styles/_message-quick-reactions.css +105 -32
  104. package/src/styles/_message-reactions.css +22 -32
  105. package/src/styles/_modal.css +2 -1
  106. package/src/styles/_preview-overlay.css +38 -0
  107. package/src/styles/_recovery-pin.css +97 -0
  108. package/src/styles/_tokens.css +22 -20
  109. package/src/styles/_typing-indicator.css +26 -10
  110. package/src/styles/index.css +2 -0
  111. package/src/types.ts +477 -15
  112. package/src/utils/avatarColors.ts +48 -0
  113. 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
- if (channel) {
45
- setMessages([...channel.state.latestMessages]);
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
- <ChatContext.Provider value={value}>
91
- <div className={`ermis-chat ermis-chat--${theme}`}>
92
- {children}
93
- {enableCall && CallUIView}
94
- </div>
95
- </ChatContext.Provider>
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]); // React to updateTick correctly
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 sub = channel.on('channel.updated', updateChannel);
47
- return () => sub.unsubscribe();
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 && event.user?.id !== client.userID) {
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
- const idx = prev.findIndex((ch) => ch.cid === eventCid);
52
- if (idx <= 0) {
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 idx === 0 ? [...prev] : prev;
71
+ return targetIdx === 0 ? [...prev] : prev;
55
72
  }
56
73
 
57
- const channel = prev[idx];
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(idx, 1);
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
- setChannels((prev) => {
186
- // Defensively mutate the channel's membership before grouping logic runs
187
- const eventCid =
188
- event.cid ||
189
- event.channel?.cid ||
190
- ((event as Record<string, unknown>).channel_id
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) => handleChannelCreated(event, true));
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
  }