diff --git a/.gitignore b/.gitignore index 25cef1d90..cecda401b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules .parcel-cache +.env +.env.local +.env.*.local # yarn .pnp.* diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index f55f55d58..1b7dfb82b 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -467,7 +467,16 @@ 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) { + 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 +491,50 @@ export default class EmbeddedChatApi { return await response.json(); } catch (err) { console.error(err); + return { + success: false, + error: err instanceof Error ? err.message : "Unknown error", + }; + } + } + + 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; } } diff --git a/packages/react/src/hooks/useRoomId.js b/packages/react/src/hooks/useRoomId.js new file mode 100644 index 000000000..b7853dbec --- /dev/null +++ b/packages/react/src/hooks/useRoomId.js @@ -0,0 +1,93 @@ +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: 'GENERAL', error: null } + : { roomId: 'GENERAL', error: null }; + }); + + useEffect(() => { + const resolveRoomId = async () => { + if (roomId) { + setResolvedRoomId({ roomId, error: null }); + return; + } + + if (channelName) { + if (!isUserAuthenticated) { + setResolvedRoomId({ roomId: 'GENERAL', error: null }); + 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 f3b94c7b4..d8dae6864 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'; @@ -18,12 +11,18 @@ 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'; import GlobalStyles from './GlobalStyles'; import { overrideECProps } from '../lib/overrideECProps'; +import { useRoomId } from '../hooks/useRoomId'; const EmbeddedChat = (props) => { const [config, setConfig] = useState(() => props); @@ -60,11 +59,9 @@ const EmbeddedChat = (props) => { remoteOpt = false, } = config; - const hasMounted = useRef(false); 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, @@ -72,6 +69,7 @@ const EmbeddedChat = (props) => { setUserId: setAuthenticatedUserId, setName: setAuthenticatedName, setRoles: setAuthenticatedUserRoles, + isUserAuthenticated, } = useUserStore((state) => ({ isUserAuthenticated: state.isUserAuthenticated, setIsUserAuthenticated: state.setIsUserAuthenticated, @@ -89,37 +87,69 @@ const EmbeddedChat = (props) => { ); } - const initializeRCInstance = useCallback(() => { - const newRCInstance = new EmbeddedChatApi(host, roomId, { - getToken, - deleteToken, - saveToken, - }); + const { getToken, saveToken, deleteToken } = getTokenStorage(secure); + const { roomId: resolvedRoomId, error: roomIdError } = useRoomId( + roomId, + channelName, + host, + getToken, + deleteToken, + saveToken, + isUserAuthenticated + ); - return newRCInstance; - }, [host, roomId, getToken, deleteToken, saveToken]); + const RCInstance = useMemo(() => { + const roomIdToUse = resolvedRoomId || roomId || 'GENERAL'; + if (!roomIdToUse) { + return null; + } + try { + return new EmbeddedChatApi(host, roomIdToUse, { + getToken, + deleteToken, + saveToken, + }); + } catch (error) { + console.error('Failed to create RCInstance:', error); + return null; + } + }, [host, resolvedRoomId, roomId, getToken, deleteToken, saveToken]); - const [RCInstance, setRCInstance] = useState(() => initializeRCInstance()); + const setMessages = useMessageStore((state) => state.setMessages); + const setChannelInfo = useChannelStore((state) => state.setChannelInfo); useEffect(() => { - const reInstantiate = () => { - const newRCInstance = initializeRCInstance(); - setRCInstance(newRCInstance); - }; - - if (!hasMounted.current) { - hasMounted.current = true; + if (resolvedRoomId === null || !RCInstance) { return; } - RCInstance.close().then(reInstantiate).catch(console.error); + const cleanup = async () => { + setMessages([], false); + setChannelInfo({}); + useMessageStore.setState({ + messages: [], + threadMessages: [], + filtered: false, + threadMainMessage: null, + deletedMessage: {}, + quoteMessage: [], + editMessage: {}, + messagesOffset: 0, + isMessageLoaded: false, + }); + }; + + cleanup(); return () => { RCInstance.close().catch(console.error); }; - }, [roomId, host, initializeRCInstance]); + }, [resolvedRoomId, setMessages, setChannelInfo, RCInstance]); useEffect(() => { + if (!RCInstance) { + return; + } const autoLogin = async () => { setIsLoginIn(true); try { @@ -134,11 +164,13 @@ const EmbeddedChat = (props) => { }, [RCInstance, auth, setIsLoginIn]); useEffect(() => { + if (!RCInstance) { + return; + } RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.connect() .then(() => { - console.log(`Connected to RocketChat ${RCInstance.host}`); const { me } = user; setAuthenticatedAvatarUrl(me.avatarUrl); setAuthenticatedUsername(me.username); @@ -163,6 +195,10 @@ const EmbeddedChat = (props) => { ]); useEffect(() => { + if (!RCInstance) { + setIsSynced(true); + return; + } const getConfig = async () => { try { const appInfo = await RCInstance.getRCAppInfo(); @@ -189,7 +225,7 @@ const EmbeddedChat = (props) => { width, height, host, - roomId, + roomId: resolvedRoomId, channelName, showName, showRoles, @@ -217,16 +253,90 @@ 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 (roomIdError && !RCInstance) { + return ( + + + + + {hideHeader ? null : ( + + )} + + + + {roomIdError} + + + Please check the channel name and try again. + + + + + + + ); + } + + if (!RCInstance) { + return null; + } + return ( - +