From 41b242a79ff77aab5d82232aab3c9d0f5daca35b Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Fri, 2 Jan 2026 00:37:56 +0530 Subject: [PATCH 01/12] feat: enable channel switching by channel name - Add channelName to roomId resolution using Rocket.Chat API - Implement resolvedRoomId state management to handle dynamic room resolution - Fix re-instantiation logic to properly clear messages when switching channels - Wait for authentication before resolving channelName to prevent 401 errors - Update ChatHeader and ChatInput to prioritize channelName prop for display Fixes: Channel switching now works correctly when only channelName is provided --- .gitignore | 4 ++++ packages/api/src/EmbeddedChatApi.ts | 16 +++++++++++++++- .../react/src/views/ChatHeader/ChatHeader.js | 7 +++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 25cef1d906..73befdea65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ node_modules .parcel-cache +# Environment variables (may contain sensitive credentials) +.env +.env.local +.env.*.local # yarn .pnp.* diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 809b6f4b5c..eccf00e816 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -467,7 +467,17 @@ export default class EmbeddedChatApi { async channelInfo() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const currentUser = await this.auth.getCurrentUser(); + if (!currentUser || !currentUser.authToken || !currentUser.userId) { + // User not authenticated yet, return error response + return { + success: false, + error: "User not authenticated", + errorType: "unauthorized", + }; + } + + const { userId, authToken } = currentUser; const response = await fetch( `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, { @@ -482,6 +492,10 @@ export default class EmbeddedChatApi { return await response.json(); } catch (err) { console.error(err); + return { + success: false, + error: err instanceof Error ? err.message : "Unknown error", + }; } } diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 9143598d30..4538d627ef 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -183,6 +183,13 @@ const ChatHeader = ({ setIsChannelReadOnly(true); setMessageAllowed(); } + } else if ( + 'errorType' in res && + res.errorType === 'unauthorized' + ) { + // User not authenticated yet, wait and retry + // The effect will re-run when isUserAuthenticated changes + return; } else if ( 'errorType' in res && res.errorType === 'error-room-not-found' From 4612126960708bb30eab05931789c60ceaab2068 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Fri, 2 Jan 2026 00:55:17 +0530 Subject: [PATCH 02/12] fix: resolve lint errors (prettier formatting and code style) --- packages/react/src/views/ChatHeader/ChatHeader.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 4538d627ef..960f437e5e 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -183,13 +183,9 @@ const ChatHeader = ({ setIsChannelReadOnly(true); setMessageAllowed(); } - } else if ( - 'errorType' in res && - res.errorType === 'unauthorized' - ) { + } else if ('errorType' in res && res.errorType === 'unauthorized') { // User not authenticated yet, wait and retry // The effect will re-run when isUserAuthenticated changes - return; } else if ( 'errorType' in res && res.errorType === 'error-room-not-found' From 1cb376d82f2afac1190e30fd13070e9a1ef0593f Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Fri, 2 Jan 2026 01:12:50 +0530 Subject: [PATCH 03/12] checking final Functionality --- packages/react/src/views/EmbeddedChat.js | 190 +++++++++++++++++++++-- 1 file changed, 176 insertions(+), 14 deletions(-) diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index f3b94c7b48..2190d7d143 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -18,7 +18,12 @@ import { import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; import { RCInstanceProvider } from '../context/RCInstance'; -import { useUserStore, useLoginStore, useMessageStore } from '../store'; +import { + useUserStore, + useLoginStore, + useMessageStore, + useChannelStore, +} from '../store'; import DefaultTheme from '../theme/DefaultTheme'; import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; @@ -27,9 +32,23 @@ import { overrideECProps } from '../lib/overrideECProps'; const EmbeddedChat = (props) => { const [config, setConfig] = useState(() => props); + // Track if roomId was explicitly provided (not just the default) + const [explicitRoomId, setExplicitRoomId] = useState(() => props.roomId); + // Don't default to GENERAL if channelName is provided - wait for resolution + const [resolvedRoomId, setResolvedRoomId] = useState(() => { + // If roomId is explicitly provided, use it + if (props.roomId) { + return props.roomId; + } + // If channelName is provided, we'll resolve it, so start with null to indicate pending + // Otherwise default to GENERAL + return props.channelName ? null : 'GENERAL'; + }); useEffect(() => { setConfig(props); + // Track if roomId is explicitly provided + setExplicitRoomId(props.roomId); }, [props]); const { @@ -61,6 +80,7 @@ const EmbeddedChat = (props) => { } = config; const hasMounted = useRef(false); + const previousResolvedRoomId = useRef(resolvedRoomId); const { classNames, styleOverrides } = useComponentOverrides('EmbeddedChat'); const [fullScreen, setFullScreen] = useState(false); const [isSynced, setIsSynced] = useState(!remoteOpt); @@ -72,6 +92,7 @@ const EmbeddedChat = (props) => { setUserId: setAuthenticatedUserId, setName: setAuthenticatedName, setRoles: setAuthenticatedUserRoles, + isUserAuthenticated, } = useUserStore((state) => ({ isUserAuthenticated: state.isUserAuthenticated, setIsUserAuthenticated: state.setIsUserAuthenticated, @@ -90,34 +111,176 @@ const EmbeddedChat = (props) => { } const initializeRCInstance = useCallback(() => { - const newRCInstance = new EmbeddedChatApi(host, roomId, { + // Use resolvedRoomId or fallback to GENERAL if not resolved yet + // This ensures we always have a valid roomId for the RCInstance + const roomIdToUse = resolvedRoomId || 'GENERAL'; + const newRCInstance = new EmbeddedChatApi(host, roomIdToUse, { getToken, deleteToken, saveToken, }); return newRCInstance; - }, [host, roomId, getToken, deleteToken, saveToken]); + }, [host, resolvedRoomId, getToken, deleteToken, saveToken]); - const [RCInstance, setRCInstance] = useState(() => initializeRCInstance()); + // Initialize RCInstance - use GENERAL temporarily if resolving channelName + const [RCInstance, setRCInstance] = useState(() => { + // If we're resolving channelName (resolvedRoomId is null), use GENERAL temporarily + // It will be re-instantiated when resolution completes + const initialRoomId = resolvedRoomId || 'GENERAL'; + return new EmbeddedChatApi(host, initialRoomId, { + getToken, + deleteToken, + saveToken, + }); + }); + const setMessages = useMessageStore((state) => state.setMessages); + const setChannelInfo = useChannelStore((state) => state.setChannelInfo); + // Resolve roomId from channelName when channelName is provided and no explicit roomId + // Priority: explicit roomId prop > resolved channelName > 'GENERAL' useEffect(() => { - const reInstantiate = () => { + const resolveRoomId = async () => { + // If roomId is explicitly provided, use it directly + if (explicitRoomId) { + setResolvedRoomId(explicitRoomId); + return; + } + + // If channelName is provided but no explicit roomId, resolve it to roomId + if (channelName) { + try { + // We need auth token, but RCInstance might not be ready yet + if (!RCInstance) { + // Don't set resolvedRoomId yet - wait for RCInstance + return; + } + + // Wait for authentication before resolving + if (!isUserAuthenticated) { + // Not authenticated yet, wait for authentication - don't set to GENERAL + return; + } + + const currentUser = await RCInstance.auth.getCurrentUser(); + const authToken = currentUser?.authToken; + const userId = currentUser?.userId || currentUser?._id; + + if (!authToken || !userId) { + // No auth token available, wait - don't set to GENERAL yet + return; + } + + // Resolve channelName to roomId using the API + const response = await fetch( + `${host}/api/v1/rooms.info?roomName=${encodeURIComponent( + channelName + )}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': authToken, + 'X-User-Id': userId, + }, + } + ); + + if (!response.ok) { + // Handle 401 or other errors + if (response.status === 401) { + // Don't set to GENERAL - wait for auth to complete + return; + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (data?.success && data?.room?._id) { + // Successfully resolved channelName to roomId + setResolvedRoomId(data.room._id); + } else { + // Fallback to GENERAL if resolution fails + setResolvedRoomId('GENERAL'); + } + } catch (error) { + // Fallback to GENERAL on error + setResolvedRoomId('GENERAL'); + } + } else { + // No channelName and no explicit roomId, use GENERAL + setResolvedRoomId('GENERAL'); + } + }; + + resolveRoomId(); + }, [channelName, explicitRoomId, host, RCInstance, isUserAuthenticated]); + + useEffect(() => { + const reInstantiate = async () => { + // On first mount, mark as mounted + if (!hasMounted.current) { + hasMounted.current = true; + previousResolvedRoomId.current = resolvedRoomId; + // If resolvedRoomId is null, we're waiting for resolution - don't do anything yet + if (resolvedRoomId === null) { + return; + } + // If resolvedRoomId is already set on first mount, we're good (roomId was provided) + return; + } + + // If resolvedRoomId is null, we're still waiting for resolution + // Don't re-instantiate yet - wait for resolution to complete + if (resolvedRoomId === null) { + return; + } + + // Check if resolvedRoomId actually changed + if (previousResolvedRoomId.current === resolvedRoomId) { + // No change, don't re-instantiate + return; + } + + // Update the ref + previousResolvedRoomId.current = resolvedRoomId; + + // First, close the old connection completely + await RCInstance.close(); + + // Clear messages and channel info AFTER closing old connection + setMessages([], false); + setChannelInfo({}); + useMessageStore.setState({ + messages: [], + threadMessages: [], + filtered: false, + threadMainMessage: null, + deletedMessage: {}, + quoteMessage: [], + editMessage: {}, + messagesOffset: 0, + isMessageLoaded: false, + }); + + // Create new instance with new roomId const newRCInstance = initializeRCInstance(); setRCInstance(newRCInstance); }; - if (!hasMounted.current) { - hasMounted.current = true; - return; - } - - RCInstance.close().then(reInstantiate).catch(console.error); + reInstantiate().catch(console.error); return () => { RCInstance.close().catch(console.error); }; - }, [roomId, host, initializeRCInstance]); + }, [ + resolvedRoomId, + host, + initializeRCInstance, + setMessages, + setChannelInfo, + RCInstance, + ]); useEffect(() => { const autoLogin = async () => { @@ -138,7 +301,6 @@ const EmbeddedChat = (props) => { if (user) { RCInstance.connect() .then(() => { - console.log(`Connected to RocketChat ${RCInstance.host}`); const { me } = user; setAuthenticatedAvatarUrl(me.avatarUrl); setAuthenticatedUsername(me.username); @@ -189,7 +351,7 @@ const EmbeddedChat = (props) => { width, height, host, - roomId, + roomId: resolvedRoomId, channelName, showName, showRoles, From b47d66c22db698ef2fb2e72fe8c3659e0fb6b893 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sat, 3 Jan 2026 14:42:06 +0530 Subject: [PATCH 04/12] refactor: remove comments added during channel name switching feature implementation --- packages/api/src/EmbeddedChatApi.ts | 1 - .../react/src/views/ChatHeader/ChatHeader.js | 2 - packages/react/src/views/EmbeddedChat.js | 38 ------------------- 3 files changed, 41 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index eccf00e816..6cc7f0a11b 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -469,7 +469,6 @@ export default class EmbeddedChatApi { try { const currentUser = await this.auth.getCurrentUser(); if (!currentUser || !currentUser.authToken || !currentUser.userId) { - // User not authenticated yet, return error response return { success: false, error: "User not authenticated", diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 960f437e5e..1c313c8394 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -184,8 +184,6 @@ const ChatHeader = ({ setMessageAllowed(); } } else if ('errorType' in res && res.errorType === 'unauthorized') { - // User not authenticated yet, wait and retry - // The effect will re-run when isUserAuthenticated changes } else if ( 'errorType' in res && res.errorType === 'error-room-not-found' diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 2190d7d143..7c2166982a 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -32,22 +32,16 @@ import { overrideECProps } from '../lib/overrideECProps'; const EmbeddedChat = (props) => { const [config, setConfig] = useState(() => props); - // Track if roomId was explicitly provided (not just the default) const [explicitRoomId, setExplicitRoomId] = useState(() => props.roomId); - // Don't default to GENERAL if channelName is provided - wait for resolution const [resolvedRoomId, setResolvedRoomId] = useState(() => { - // If roomId is explicitly provided, use it if (props.roomId) { return props.roomId; } - // If channelName is provided, we'll resolve it, so start with null to indicate pending - // Otherwise default to GENERAL return props.channelName ? null : 'GENERAL'; }); useEffect(() => { setConfig(props); - // Track if roomId is explicitly provided setExplicitRoomId(props.roomId); }, [props]); @@ -111,8 +105,6 @@ const EmbeddedChat = (props) => { } const initializeRCInstance = useCallback(() => { - // Use resolvedRoomId or fallback to GENERAL if not resolved yet - // This ensures we always have a valid roomId for the RCInstance const roomIdToUse = resolvedRoomId || 'GENERAL'; const newRCInstance = new EmbeddedChatApi(host, roomIdToUse, { getToken, @@ -123,10 +115,7 @@ const EmbeddedChat = (props) => { return newRCInstance; }, [host, resolvedRoomId, getToken, deleteToken, saveToken]); - // Initialize RCInstance - use GENERAL temporarily if resolving channelName const [RCInstance, setRCInstance] = useState(() => { - // If we're resolving channelName (resolvedRoomId is null), use GENERAL temporarily - // It will be re-instantiated when resolution completes const initialRoomId = resolvedRoomId || 'GENERAL'; return new EmbeddedChatApi(host, initialRoomId, { getToken, @@ -137,28 +126,20 @@ const EmbeddedChat = (props) => { const setMessages = useMessageStore((state) => state.setMessages); const setChannelInfo = useChannelStore((state) => state.setChannelInfo); - // Resolve roomId from channelName when channelName is provided and no explicit roomId - // Priority: explicit roomId prop > resolved channelName > 'GENERAL' useEffect(() => { const resolveRoomId = async () => { - // If roomId is explicitly provided, use it directly if (explicitRoomId) { setResolvedRoomId(explicitRoomId); return; } - // If channelName is provided but no explicit roomId, resolve it to roomId if (channelName) { try { - // We need auth token, but RCInstance might not be ready yet if (!RCInstance) { - // Don't set resolvedRoomId yet - wait for RCInstance return; } - // Wait for authentication before resolving if (!isUserAuthenticated) { - // Not authenticated yet, wait for authentication - don't set to GENERAL return; } @@ -167,11 +148,9 @@ const EmbeddedChat = (props) => { const userId = currentUser?.userId || currentUser?._id; if (!authToken || !userId) { - // No auth token available, wait - don't set to GENERAL yet return; } - // Resolve channelName to roomId using the API const response = await fetch( `${host}/api/v1/rooms.info?roomName=${encodeURIComponent( channelName @@ -187,9 +166,7 @@ const EmbeddedChat = (props) => { ); if (!response.ok) { - // Handle 401 or other errors if (response.status === 401) { - // Don't set to GENERAL - wait for auth to complete return; } throw new Error(`HTTP error! status: ${response.status}`); @@ -197,18 +174,14 @@ const EmbeddedChat = (props) => { const data = await response.json(); if (data?.success && data?.room?._id) { - // Successfully resolved channelName to roomId setResolvedRoomId(data.room._id); } else { - // Fallback to GENERAL if resolution fails setResolvedRoomId('GENERAL'); } } catch (error) { - // Fallback to GENERAL on error setResolvedRoomId('GENERAL'); } } else { - // No channelName and no explicit roomId, use GENERAL setResolvedRoomId('GENERAL'); } }; @@ -218,37 +191,27 @@ const EmbeddedChat = (props) => { useEffect(() => { const reInstantiate = async () => { - // On first mount, mark as mounted if (!hasMounted.current) { hasMounted.current = true; previousResolvedRoomId.current = resolvedRoomId; - // If resolvedRoomId is null, we're waiting for resolution - don't do anything yet if (resolvedRoomId === null) { return; } - // If resolvedRoomId is already set on first mount, we're good (roomId was provided) return; } - // If resolvedRoomId is null, we're still waiting for resolution - // Don't re-instantiate yet - wait for resolution to complete if (resolvedRoomId === null) { return; } - // Check if resolvedRoomId actually changed if (previousResolvedRoomId.current === resolvedRoomId) { - // No change, don't re-instantiate return; } - // Update the ref previousResolvedRoomId.current = resolvedRoomId; - // First, close the old connection completely await RCInstance.close(); - // Clear messages and channel info AFTER closing old connection setMessages([], false); setChannelInfo({}); useMessageStore.setState({ @@ -263,7 +226,6 @@ const EmbeddedChat = (props) => { isMessageLoaded: false, }); - // Create new instance with new roomId const newRCInstance = initializeRCInstance(); setRCInstance(newRCInstance); }; From 1cbe0afa599a1fa8f6cd5a5ab6b0922e26fc8eb9 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sat, 3 Jan 2026 15:03:40 +0530 Subject: [PATCH 05/12] refactor: remove comment from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73befdea65..cecda401b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules .parcel-cache -# Environment variables (may contain sensitive credentials) .env .env.local .env.*.local From f19ab0114a8d21d958483022799141b43d4ebde0 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sat, 3 Jan 2026 19:37:25 +0530 Subject: [PATCH 06/12] fix: remove empty block statement to resolve lint error --- packages/react/src/views/ChatHeader/ChatHeader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 1c313c8394..9143598d30 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -183,7 +183,6 @@ const ChatHeader = ({ setIsChannelReadOnly(true); setMessageAllowed(); } - } else if ('errorType' in res && res.errorType === 'unauthorized') { } else if ( 'errorType' in res && res.errorType === 'error-room-not-found' From 903b7b572ddce34ea4b47ca103b9dbd3c60c8e70 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sat, 3 Jan 2026 19:47:21 +0530 Subject: [PATCH 07/12] fix: format EmbeddedChatApi.ts to pass format check --- packages/api/src/EmbeddedChatApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 6cc7f0a11b..ff5fd62ada 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -475,7 +475,7 @@ export default class EmbeddedChatApi { errorType: "unauthorized", }; } - + const { userId, authToken } = currentUser; const response = await fetch( `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, From 551819f6e348e9b534ec3a983dfe5b68fb499e27 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sun, 18 Jan 2026 03:53:04 +0530 Subject: [PATCH 08/12] feat: implement channel switching by name with proper error handling - Add getRoomIdByName method to EmbeddedChatApi for resolving channel names to room IDs - Create useRoomId custom hook to encapsulate room ID resolution logic - Refactor EmbeddedChat to use key-based approach instead of manual state tracking - Remove fallback to GENERAL when channel name resolution fails - Display clear error messages when channel not found or inaccessible - Show error UI and toast notifications for failed channel resolution - Maintain backward compatibility with explicit roomId prop --- packages/api/src/EmbeddedChatApi.ts | 40 ++++ packages/react/src/hooks/useRoomId.js | 92 +++++++++ packages/react/src/views/EmbeddedChat.js | 251 +++++++++++------------ 3 files changed, 253 insertions(+), 130 deletions(-) create mode 100644 packages/react/src/hooks/useRoomId.js diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 18c2b5f734..1b7dfb82b3 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -498,6 +498,46 @@ export default class EmbeddedChatApi { } } + async getRoomIdByName(channelName: string): Promise { + try { + const currentUser = await this.auth.getCurrentUser(); + if (!currentUser || !currentUser.authToken || !currentUser.userId) { + return null; + } + + const { userId, authToken } = currentUser; + const response = await fetch( + `${this.host}/api/v1/rooms.info?roomName=${encodeURIComponent( + channelName + )}`, + { + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + method: "GET", + } + ); + + if (!response.ok) { + if (response.status === 401) { + return null; + } + return null; + } + + const data = await response.json(); + if (data?.success === true && data?.room?._id) { + return data.room._id; + } + return null; + } catch (err) { + console.error(err); + return null; + } + } + async getRoomInfo() { try { const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; diff --git a/packages/react/src/hooks/useRoomId.js b/packages/react/src/hooks/useRoomId.js new file mode 100644 index 0000000000..401ba39034 --- /dev/null +++ b/packages/react/src/hooks/useRoomId.js @@ -0,0 +1,92 @@ +import { useState, useEffect } from 'react'; +import { EmbeddedChatApi } from '@embeddedchat/api'; + +/** + * Custom hook to resolve roomId from either explicit roomId or channelName. + * Returns an object with: + * - roomId: The resolved room ID, or null if resolution is pending/failed + * - error: Error message if resolution failed, or null if successful/pending + * + * @param {string|null|undefined} roomId - Explicit room ID + * @param {string|null|undefined} channelName - Channel name to resolve + * @param {string} host - Rocket.Chat host URL + * @param {Function} getToken - Token getter function + * @param {Function} deleteToken - Token deleter function + * @param {Function} saveToken - Token saver function + * @param {boolean} isUserAuthenticated - Whether user is authenticated + * @returns {{roomId: string|null, error: string|null}} - Resolved room ID and error state + */ +export const useRoomId = ( + roomId, + channelName, + host, + getToken, + deleteToken, + saveToken, + isUserAuthenticated +) => { + const [resolvedRoomId, setResolvedRoomId] = useState(() => { + if (roomId) { + return { roomId, error: null }; + } + return channelName + ? { roomId: null, error: null } + : { roomId: 'GENERAL', error: null }; + }); + + useEffect(() => { + const resolveRoomId = async () => { + if (roomId) { + setResolvedRoomId({ roomId, error: null }); + return; + } + + if (channelName) { + if (!isUserAuthenticated) { + return; + } + + try { + const tempRCInstance = new EmbeddedChatApi(host, 'GENERAL', { + getToken, + deleteToken, + saveToken, + }); + const roomIdFromName = await tempRCInstance.getRoomIdByName( + channelName + ); + await tempRCInstance.close().catch(console.error); + if (roomIdFromName) { + setResolvedRoomId({ roomId: roomIdFromName, error: null }); + } else { + setResolvedRoomId({ + roomId: null, + error: `Channel "${channelName}" not found or you don't have access to it.`, + }); + } + } catch (error) { + setResolvedRoomId({ + roomId: null, + error: `Failed to resolve channel "${channelName}": ${ + error.message || 'Unknown error' + }`, + }); + } + } else { + setResolvedRoomId({ roomId: 'GENERAL', error: null }); + } + }; + + resolveRoomId(); + }, [ + roomId, + channelName, + host, + getToken, + deleteToken, + saveToken, + isUserAuthenticated, + ]); + + return resolvedRoomId; +}; diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 7c2166982a..ea7bc76c43 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -1,11 +1,4 @@ -import React, { - memo, - useEffect, - useMemo, - useState, - useCallback, - useRef, -} from 'react'; +import React, { memo, useEffect, useMemo, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { EmbeddedChatApi } from '@embeddedchat/api'; @@ -14,6 +7,7 @@ import { ToastBarProvider, useComponentOverrides, ThemeProvider, + useToastBarDispatch, } from '@embeddedchat/ui-elements'; import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; @@ -29,20 +23,13 @@ import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; import GlobalStyles from './GlobalStyles'; import { overrideECProps } from '../lib/overrideECProps'; +import { useRoomId } from '../hooks/useRoomId'; const EmbeddedChat = (props) => { const [config, setConfig] = useState(() => props); - const [explicitRoomId, setExplicitRoomId] = useState(() => props.roomId); - const [resolvedRoomId, setResolvedRoomId] = useState(() => { - if (props.roomId) { - return props.roomId; - } - return props.channelName ? null : 'GENERAL'; - }); useEffect(() => { setConfig(props); - setExplicitRoomId(props.roomId); }, [props]); const { @@ -73,12 +60,9 @@ const EmbeddedChat = (props) => { remoteOpt = false, } = config; - const hasMounted = useRef(false); - const previousResolvedRoomId = useRef(resolvedRoomId); const { classNames, styleOverrides } = useComponentOverrides('EmbeddedChat'); const [fullScreen, setFullScreen] = useState(false); const [isSynced, setIsSynced] = useState(!remoteOpt); - const { getToken, saveToken, deleteToken } = getTokenStorage(secure); const { setIsUserAuthenticated, setUsername: setAuthenticatedUsername, @@ -104,114 +88,49 @@ const EmbeddedChat = (props) => { ); } - const initializeRCInstance = useCallback(() => { + const { getToken, saveToken, deleteToken } = getTokenStorage(secure); + const { roomId: resolvedRoomId, error: roomIdError } = useRoomId( + roomId, + channelName, + host, + getToken, + deleteToken, + saveToken, + isUserAuthenticated + ); + + const dispatchToastMessage = useToastBarDispatch(); + + const RCInstance = useMemo(() => { + if (resolvedRoomId === null) { + return null; + } const roomIdToUse = resolvedRoomId || 'GENERAL'; - const newRCInstance = new EmbeddedChatApi(host, roomIdToUse, { + return new EmbeddedChatApi(host, roomIdToUse, { getToken, deleteToken, saveToken, }); - - return newRCInstance; }, [host, resolvedRoomId, getToken, deleteToken, saveToken]); - const [RCInstance, setRCInstance] = useState(() => { - const initialRoomId = resolvedRoomId || 'GENERAL'; - return new EmbeddedChatApi(host, initialRoomId, { - getToken, - deleteToken, - saveToken, - }); - }); const setMessages = useMessageStore((state) => state.setMessages); const setChannelInfo = useChannelStore((state) => state.setChannelInfo); useEffect(() => { - const resolveRoomId = async () => { - if (explicitRoomId) { - setResolvedRoomId(explicitRoomId); - return; - } - - if (channelName) { - try { - if (!RCInstance) { - return; - } - - if (!isUserAuthenticated) { - return; - } - - const currentUser = await RCInstance.auth.getCurrentUser(); - const authToken = currentUser?.authToken; - const userId = currentUser?.userId || currentUser?._id; - - if (!authToken || !userId) { - return; - } - - const response = await fetch( - `${host}/api/v1/rooms.info?roomName=${encodeURIComponent( - channelName - )}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': authToken, - 'X-User-Id': userId, - }, - } - ); - - if (!response.ok) { - if (response.status === 401) { - return; - } - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - if (data?.success && data?.room?._id) { - setResolvedRoomId(data.room._id); - } else { - setResolvedRoomId('GENERAL'); - } - } catch (error) { - setResolvedRoomId('GENERAL'); - } - } else { - setResolvedRoomId('GENERAL'); - } - }; - - resolveRoomId(); - }, [channelName, explicitRoomId, host, RCInstance, isUserAuthenticated]); + if (roomIdError) { + dispatchToastMessage({ + type: 'error', + message: roomIdError, + }); + } + }, [roomIdError, dispatchToastMessage]); useEffect(() => { - const reInstantiate = async () => { - if (!hasMounted.current) { - hasMounted.current = true; - previousResolvedRoomId.current = resolvedRoomId; - if (resolvedRoomId === null) { - return; - } - return; - } - - if (resolvedRoomId === null) { - return; - } - - if (previousResolvedRoomId.current === resolvedRoomId) { - return; - } - - previousResolvedRoomId.current = resolvedRoomId; - - await RCInstance.close(); + if (resolvedRoomId === null || !RCInstance) { + return; + } + const cleanup = async () => { setMessages([], false); setChannelInfo({}); useMessageStore.setState({ @@ -225,26 +144,19 @@ const EmbeddedChat = (props) => { messagesOffset: 0, isMessageLoaded: false, }); - - const newRCInstance = initializeRCInstance(); - setRCInstance(newRCInstance); }; - reInstantiate().catch(console.error); + cleanup(); return () => { RCInstance.close().catch(console.error); }; - }, [ - resolvedRoomId, - host, - initializeRCInstance, - setMessages, - setChannelInfo, - RCInstance, - ]); + }, [resolvedRoomId, setMessages, setChannelInfo, RCInstance]); useEffect(() => { + if (!RCInstance) { + return; + } const autoLogin = async () => { setIsLoginIn(true); try { @@ -259,6 +171,9 @@ const EmbeddedChat = (props) => { }, [RCInstance, auth, setIsLoginIn]); useEffect(() => { + if (!RCInstance) { + return; + } RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.connect() @@ -287,6 +202,10 @@ const EmbeddedChat = (props) => { ]); useEffect(() => { + if (!RCInstance) { + setIsSynced(true); + return; + } const getConfig = async () => { try { const appInfo = await RCInstance.getRCAppInfo(); @@ -341,16 +260,88 @@ const EmbeddedChat = (props) => { ] ); - const RCContextValue = useMemo( - () => ({ RCInstance, ECOptions }), - [RCInstance, ECOptions] - ); + const RCContextValue = useMemo(() => { + if (!RCInstance) { + return { RCInstance: null, ECOptions }; + } + return { RCInstance, ECOptions }; + }, [RCInstance, ECOptions]); if (!isSynced) return null; + if (!RCInstance) { + return ( + + + + + {hideHeader ? null : ( + + )} + + + + {roomIdError || 'Loading channel...'} + + {roomIdError && ( + + Please check the channel name and try again. + + )} + + + + + + ); + } + return ( - + Date: Sun, 18 Jan 2026 04:04:20 +0530 Subject: [PATCH 09/12] fix: allow component to render when waiting for authentication - Use temporary 'GENERAL' roomId when channelName is provided but user is not authenticated yet - Only show error UI when there's an actual error, not when waiting for auth - Fixes e2e tests that were failing due to component not rendering --- packages/react/src/hooks/useRoomId.js | 3 ++- packages/react/src/views/EmbeddedChat.js | 26 +++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/react/src/hooks/useRoomId.js b/packages/react/src/hooks/useRoomId.js index 401ba39034..b7853dbecf 100644 --- a/packages/react/src/hooks/useRoomId.js +++ b/packages/react/src/hooks/useRoomId.js @@ -30,7 +30,7 @@ export const useRoomId = ( return { roomId, error: null }; } return channelName - ? { roomId: null, error: null } + ? { roomId: 'GENERAL', error: null } : { roomId: 'GENERAL', error: null }; }); @@ -43,6 +43,7 @@ export const useRoomId = ( if (channelName) { if (!isUserAuthenticated) { + setResolvedRoomId({ roomId: 'GENERAL', error: null }); return; } diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index ea7bc76c43..dfadfa9edc 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -269,7 +269,7 @@ const EmbeddedChat = (props) => { if (!isSynced) return null; - if (!RCInstance) { + if (!RCInstance && roomIdError) { return ( { margin-bottom: 8px; `} > - {roomIdError || 'Loading channel...'} + {roomIdError} + + + Please check the channel name and try again. - {roomIdError && ( - - Please check the channel name and try again. - - )} @@ -336,6 +334,10 @@ const EmbeddedChat = (props) => { ); } + if (!RCInstance) { + return null; + } + return ( Date: Sun, 18 Jan 2026 18:31:23 +0530 Subject: [PATCH 10/12] fix: ensure RCInstance is always created when roomId is provided - Use roomId prop as fallback when resolvedRoomId is not available - Ensure component renders when explicit roomId is provided in e2e tests - Fixes e2e test failures where component was not rendering --- packages/react/src/views/EmbeddedChat.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index dfadfa9edc..72c21dd77e 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -102,16 +102,18 @@ const EmbeddedChat = (props) => { const dispatchToastMessage = useToastBarDispatch(); const RCInstance = useMemo(() => { - if (resolvedRoomId === null) { + const roomIdToUse = resolvedRoomId || roomId || 'GENERAL'; + try { + return new EmbeddedChatApi(host, roomIdToUse, { + getToken, + deleteToken, + saveToken, + }); + } catch (error) { + console.error('Failed to create RCInstance:', error); return null; } - const roomIdToUse = resolvedRoomId || 'GENERAL'; - return new EmbeddedChatApi(host, roomIdToUse, { - getToken, - deleteToken, - saveToken, - }); - }, [host, resolvedRoomId, getToken, deleteToken, saveToken]); + }, [host, resolvedRoomId, roomId, getToken, deleteToken, saveToken]); const setMessages = useMessageStore((state) => state.setMessages); const setChannelInfo = useChannelStore((state) => state.setChannelInfo); @@ -269,7 +271,7 @@ const EmbeddedChat = (props) => { if (!isSynced) return null; - if (!RCInstance && roomIdError) { + if (roomIdError && !RCInstance) { return ( Date: Sun, 18 Jan 2026 19:26:22 +0530 Subject: [PATCH 11/12] fix: avoid toast hook before provider in EmbeddedChat - Remove toast dispatch usage before ToastBarProvider is mounted - Prevent render crash that hid the embedded chat in e2e tests --- packages/react/src/views/EmbeddedChat.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 72c21dd77e..23062b24b4 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -7,7 +7,6 @@ import { ToastBarProvider, useComponentOverrides, ThemeProvider, - useToastBarDispatch, } from '@embeddedchat/ui-elements'; import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; @@ -99,8 +98,6 @@ const EmbeddedChat = (props) => { isUserAuthenticated ); - const dispatchToastMessage = useToastBarDispatch(); - const RCInstance = useMemo(() => { const roomIdToUse = resolvedRoomId || roomId || 'GENERAL'; try { @@ -118,15 +115,6 @@ const EmbeddedChat = (props) => { const setMessages = useMessageStore((state) => state.setMessages); const setChannelInfo = useChannelStore((state) => state.setChannelInfo); - useEffect(() => { - if (roomIdError) { - dispatchToastMessage({ - type: 'error', - message: roomIdError, - }); - } - }, [roomIdError, dispatchToastMessage]); - useEffect(() => { if (resolvedRoomId === null || !RCInstance) { return; From 3b2e2c0dc3357018a4616e2dc5d99a3b46012149 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sun, 18 Jan 2026 20:07:05 +0530 Subject: [PATCH 12/12] fix: ensure RCInstance is always created with valid roomId - Add null check before creating RCInstance to prevent errors - Ensure component always renders when roomId is provided - Fixes e2e test failures where component was not visible --- packages/react/src/views/EmbeddedChat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 23062b24b4..d8dae68645 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -100,6 +100,9 @@ const EmbeddedChat = (props) => { const RCInstance = useMemo(() => { const roomIdToUse = resolvedRoomId || roomId || 'GENERAL'; + if (!roomIdToUse) { + return null; + } try { return new EmbeddedChatApi(host, roomIdToUse, { getToken,