AFc{#H>Oo0qNobS<9YarQdsQBYn7nOl=>@)f3k(zn-&;Z1C+ zhLn0SUki}R8f#>uUo*(VZ^RcdYR*=XxJ*=eHIuB4#n$3iUtVm*h&Q*wG1aOoR&T`{ zxAo4R6=s#py5YbpTGJ{nD4^ZV;rQ?S2$KXsIkl^RWDq5<;SR++j*G0|aq4)w{ z5f2Q$%~n=NAJ6?l_n?2&eO5VkY(>j!^_#C_L|bGlx(3JNK3oWAc&?`A61Zg8+-OmY zU R{g6b8J+sybx}&`bEteuNSo`dj?#-HXTQL?^{ND*a}k503PVAGYwzIO!&en zPZZBn9`MzL!|?1oZ4Iv3i&nM9`DmO&5#OW_9U13O z>f4Nx!RwG47|!R+aCrs)p=JnDoU;c8tFo$V_`x$)4%>esOt)-Bz*GH-nXh#8j1sfU z!`1V*YKSf$1EVC=A!09=+4%UC <42Y)pHv_F$}KEmt(k8JH+{ah z9G8!&ZywIatbJpL8{ak6c{tU-8;YdK5Z7MC=cD&c9eEr+X4jHo{_(Im5;!@nv7T(N z8S+i(9CIogaYq?;CyRR}GHZS$nMqrc_2{TRCq7Z`)33_6CpVQoTilbT{8{i2_aYu= zzAxr1W?NlF25wDrOyn*n!Gc%tf;DnQ B9K+vnNH!Fjh?DwM8cY@6S+J6rjb z@@%$JDvfqo%~`K{x~QGh^ZN^sQD EmswV& zm0`bX6=BvkrH}8c>oblueP;hp1bO+%+s{;&6|>g)mfxCPr^dDTe+s;^ X=G*J+}U;`LN2d@b#;d4uTd nQSL0v_tVZ zMrPkL_nLg>)AuyPJ35usaF}=!zsLVS23741?Q}k;!`pqYbG%z-Pw|ZAKkl^se>3F{ A{Qv*} literal 0 HcmV?d00001 diff --git a/packages/react/src/hooks/useFetchChatData.js b/packages/react/src/hooks/useFetchChatData.js index 3aa03cbe2c..f92719cfbb 100644 --- a/packages/react/src/hooks/useFetchChatData.js +++ b/packages/react/src/hooks/useFetchChatData.js @@ -152,7 +152,8 @@ const useFetchChatData = (showRoles) => { const fetchedRoles = await RCInstance.getUserRoles(); const fetchedAdmins = fetchedRoles?.result; - const adminUsernames = fetchedAdmins?.map((user) => user.username) || []; + const adminUsernames = + fetchedAdmins?.map((user) => user.username) || []; setAdmins(adminUsernames); const rolesObj = diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 211cc02aa2..9ed9075b5d 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -134,7 +134,9 @@ const ChatHeader = ({ }; const setCanSendMsg = useUserStore((state) => state.setCanSendMsg); const authenticatedUserId = useUserStore((state) => state.userId); - const { getToken, saveToken, deleteToken } = getTokenStorage(ECOptions?.secure); + const { getToken, saveToken, deleteToken } = getTokenStorage( + ECOptions?.secure + ); const handleLogout = useCallback(async () => { try { await RCInstance.logout(); diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index d5aa84db32..2e77db92d7 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -12,6 +12,7 @@ import { EmbeddedChatApi } from '@embeddedchat/api'; import { Box, ToastBarProvider, + useToastBarDispatch, useComponentOverrides, ThemeProvider, } from '@embeddedchat/ui-elements'; @@ -88,6 +89,7 @@ const EmbeddedChat = (props) => { })); const setIsLoginIn = useLoginStore((state) => state.setIsLoginIn); + const dispatchToastMessage = useToastBarDispatch(); if (isClosable && !setClosableState) { throw Error( 'Please provide a setClosableState to props when isClosable = true' diff --git a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js index abe4715d52..5c963f964d 100644 --- a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js +++ b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js @@ -174,65 +174,64 @@ export const MessageAggregator = ({ )} {uniqueMessageList.map((msg, index, arr) => { - const newDay = isMessageNewDay(msg, arr[index - 1]); - if (!messageRendered && shouldRender(msg)) { - setMessageRendered(true); - } + const newDay = isMessageNewDay(msg, arr[index - 1]); + if (!messageRendered && shouldRender(msg)) { + setMessageRendered(true); + } - return ( - - {type === 'message' && newDay && ( - - ); - } - )} +- {format(new Date(msg.ts), 'MMMM d, yyyy')} - - )} - {type === 'file' ? ( -+ {type === 'message' && newDay && ( + + {format(new Date(msg.ts), 'MMMM d, yyyy')} + + )} + {type === 'file' ? ( ++ ) : ( + + - )} -- ) : ( - + + setJumpToMessage(msg)} + css={{ + position: 'relative', + zIndex: 10, + marginRight: '5px', }} > - - - setJumpToMessage(msg)} - css={{ - position: 'relative', - zIndex: 10, - marginRight: '5px', - }} - > - -- + + + )} + + ); + })} )} diff --git a/packages/react/src/views/MessageList/MessageList.js b/packages/react/src/views/MessageList/MessageList.js index 962cf8e03e..5d7c3b7f1b 100644 --- a/packages/react/src/views/MessageList/MessageList.js +++ b/packages/react/src/views/MessageList/MessageList.js @@ -27,9 +27,12 @@ const MessageList = ({ () => messages.filter((msg) => !msg.tmid).reverse(), [messages] ); - + const reportedMessage = useMemo( - () => (messageToReport ? messages.find((msg) => msg._id === messageToReport) : null), + () => + messageToReport + ? messages.find((msg) => msg._id === messageToReport) + : null, [messages, messageToReport] ); @@ -87,33 +90,33 @@ const MessageList = ({ )} {filteredMessages.map((msg, index, arr) => { - const prev = arr[index - 1]; - const next = arr[index + 1]; + const prev = arr[index - 1]; + const next = arr[index + 1]; - if (!msg) return null; - const newDay = isMessageNewDay(msg, prev); - const sequential = isMessageSequential(msg, prev, 300); - const lastSequential = - sequential && isMessageLastSequential(msg, next); - const showUnreadDivider = - firstUnreadMessageId && msg._id === firstUnreadMessageId; + if (!msg) return null; + const newDay = isMessageNewDay(msg, prev); + const sequential = isMessageSequential(msg, prev, 300); + const lastSequential = + sequential && isMessageLastSequential(msg, next); + const showUnreadDivider = + firstUnreadMessageId && msg._id === firstUnreadMessageId; - return ( - - {showUnreadDivider && ( - - ); - })} + return ( +Unread Messages - )} -- + {showUnreadDivider && ( + + ); + })} {showReportMessage && (Unread Messages + )} ++ Date: Tue, 3 Feb 2026 13:35:06 +0530 Subject: [PATCH 14/33] chore: remove temporary debug files --- packages/react/eslint_embeddedchat.txt | Bin 2874 -> 0 bytes packages/react/eslint_errors.txt | Bin 26650 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/react/eslint_embeddedchat.txt delete mode 100644 packages/react/eslint_errors.txt diff --git a/packages/react/eslint_embeddedchat.txt b/packages/react/eslint_embeddedchat.txt deleted file mode 100644 index d4ac33c960e73a94f3cf63e9bb9765158dbf449f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2874 zcmd^>TTc^F5Xa})#P6_)Ml{+sfMV>ET1%)GMATpchHQ6x6MD(+7U_#0UH$#f> oc!-PLGPlCVK^z&G1EnPx>@@R(p3Z ziB%mhDYj?h5QV$BrqeNLqNl*B5jG9%9vXJ-vprzkM#?x?(!+blSkl9y0?sMCXYaU9 zj1T!1_KsQ1?8NRi@)hZfo2N+jI|?r&J3?xUy)mC~mZGcTb{& *+qc)Io!I~ WMvV)tYruE<7P9qL&=zCE7_HLbS@IDbo|Sv{3J-Sd1Nl)$qvE5u zjabWf@~3ChQLttKJ;xU1SHU=B-R4Y}lPRD27F6x-VNVRt0lg$E2X%YVw41X^7$vzE zD#gXJ>ZYt!mafpRlB@FC5_i^3-UR7+sYmD&GJ5RG>#8WSOqicA3NKwwDjHe~FBx-} z KP>c zid$5k`Mo|_rp%AgEgk2hdV shx8SOYzzN@Zy1rh>03rEnp!5UTHlX~m_ z8p?R_Dw_UEi0-e8zxsL?58$~d|Nm#CEK_wKgOkg#(x%Gx@uDjHRqMkxFGvU6c&t rNcW6~{};w@7)1krHJbC17KWvnEQi*v2tgd+iv!8!1G)z`)qMFrb;iKtALp z@(6j%NJ;*`b0~LLPxo9g=F(~nGu>TXb^6@TsZ+)O{@V$Mp%r$+ygsM;G{Ukzk3(12 zPeM;uy862x-s^AQ?(KxF(9!v^-JP?0eS4BN4Lx_PmMyj7YD+yg!fUnK)fM_{sK;aV zupE9Cei~NPsu7;(Y*VcmwXMHf;XqfPX%wF8>0X5Mhx4-|K|3Fgbhe;p?}tZfzZ2f+ z$*#^9t?ys!8pkj7`9=6Tyb4dlYWPl{zv|=HUhDUF8vjLD)Ayglx?QK2w)zIb*vmh| zv Cmuu^O5p2S=KmxLG@lRwZpG8!r#K*!*BHcM!g&uB)h`isUX=EM18e9RNLY^J`;`i zb)1F2=+~BhWpJ;me< 3|3J&K>xH 0(Y7bvsqMD#zhiH7w-UO$YL|ZtVEws`wj~%2G%928aY=craa=Mt z!&k!MTG)_`%xhF=1nu7I9Q=V(m#7a0(V4FI1P?OS*Y`V}uj-j?J=xO1-zU1t-A%!? zr8Yn6FFhlNV1R4f?dfbs-(c%dt+;=zQNFT~_zjeAKVJDpzjmcn<^xh3vHw6Adk}sr zJT`PU(weikf`3bpp#vHk5jrq_Prvq!QyK -`!h{Z9E{}O&b35eGy>K+ z!3}Mj>H#Vrs7E-H`{ }92Myq~Z-nQ<4Kzpo7z3S#yl-0zuERsvgDrjF$)1kb7kq{`L%#XDb1q3u>#HAL z>MHFc-@TH~+dIG46x0pzEL3}^c3qv(?~X=4w*I2DLPzHqxB|@rm+(x(+7%%HR`_qr zw1GeA?>ncbB9GtI^QZQ9ym{1F=oj9}`TBZDDs%(WE#Ys=sEeNJ+M9F-cw-?vG)bL? zJ64T;STD5bw48wkI5JK{BSNPY *bUrK4YX z5g#-P8UX1=k29KcHc(-!k?%b{ l{5gArGxcz_tuXc- z>xva#(OZIbY+c0qbDe{0-lMBF8~Q#~tFpE!^%rEHp+6WJv @Ue4FI#+5 zhW!oU9gfFx;%j1Q-J99dIaV=x=t$9)BofOy%nkx4Ee=J!NTF1JXSV;2dPJVlAJDlG zz7V8D4en(TLvWP{0q@S~jwFH;xBgg~hPV;$4t~L>hnwIc;*Jxw-I9JuVRp{qT|NGY z@DAbu$lN3Fw>q)&a %7rd!q1UPkq8; zt{E8tJ2u#g_uA9l(H@3d6|xt#_gRb+%Q)}3FiVW ?NiZO5ZG0IMZodxq2$;(w!iRcb{fab61 zbkRID9+j5NvvX};CQHNgd3D4;nNBAP?Vnqsj>+Y|>3A}tQKNgCmui{M3Z!?}%>zAA z{_?%)6EYF_qp5!Pp5Ce-G|r02KAv({*(Nf2*G0#}FZb<;)_79-U4%V$Y_18LuDK)q zR)kADK+os4Oao^&Hhb!gXiTn`ScIJa 6o|+cP<^<42v_e-Ai4d)j9Y-Iz|LFb1?4d_bo)e6b4*sfpvwzex9Mkg; zPcI0cUzjh%>{l^+&J{A~Hzzy88;x^7PoKZ6&f?Gyd5tWZ`F#2_X5;ZwGc5^^wROP3 zm#Y`2=gO;^_a3FyV%(8wx? 71O8o|)Ys^F~INTvXd49%eAzLoIR|nbxW~mZq!}^v(NjS#*0FGp><>ndEMUZ%it7 zh52$H#j_15-j~dh*8WM}KbhVRU|k=b=V@x+B=cFu@l39h4SgZK=2bJ)gG5=(UrnW~ zA&-k$Wao*pxg^i*;=f_@Qu!rYd($#` q;^1{4MA+21? zYSySlEPpoDY|KY2%ZC7)WEQ=?kuzfOx{58@hG{(Qs`|u~0^~!X!Qv3>;@)#=Rggn+ zJ4D&hx=iXxL+ARq$`}~W&OWq!kVjvW=7ZoW>XfFn3UL (lesth}{oH3A;77kyixZ(63gXBxp*8Ei=QnHd|*K1ct` zXYE`YR$F)5@GCm499f!V0=_yXG~6 z%vO)boo@TkNz5TrpNvJl*mWh0@k(!j-DCvTVzH_rT2ZfHXHJ=#TCCxu5{6y&syA$} z*S0ZBREDEi>x*T^8hd<1r7#d;-F?O)`VZ7K#<_Ru>KK91=FZ3T%zA8%Rj0ax6;;s8 zYyW4-HspCL#(8P9O^aG1U8Ia(ufEDO(sjsDJ(Hv{)1T%hHb1U`POld;A9#O@;(U&> zth&o}n#;P#Y)6K5AE_mcfaZy7H-KwVUMf)fO3yMV(GpLE1Mq-;j5xU{NXLWHtBdbd z;u3!a2tU AFc{#H>Oo0qNobS<9YarQdsQBYn7nOl=>@)f3k(zn-&;Z1C+ zhLn0SUki}R8f#>uUo*(VZ^RcdYR*=XxJ*=eHIuB4#n$3iUtVm*h&Q*wG1aOoR&T`{ zxAo4R6=s#py5YbpTGJ{nD4^ZV;rQ?S2$KXsIkl^RWDq5<;SR++j*G0|aq4)w{ z5f2Q$%~n=NAJ6?l_n?2&eO5VkY(>j!^_#C_L|bGlx(3JNK3oWAc&?`A61Zg8+-OmY zU R{g6b8J+sybx}&`bEteuNSo`dj?#-HXTQL?^{ND*a}k503PVAGYwzIO!&en zPZZBn9`MzL!|?1oZ4Iv3i&nM9`DmO&5#OW_9U13O z>f4Nx!RwG47|!R+aCrs)p=JnDoU;c8tFo$V_`x$)4%>esOt)-Bz*GH-nXh#8j1sfU z!`1V*YKSf$1EVC=A!09=+4%UC <42Y)pHv_F$}KEmt(k8JH+{ah z9G8!&ZywIatbJpL8{ak6c{tU-8;YdK5Z7MC=cD&c9eEr+X4jHo{_(Im5;!@nv7T(N z8S+i(9CIogaYq?;CyRR}GHZS$nMqrc_2{TRCq7Z`)33_6CpVQoTilbT{8{i2_aYu= zzAxr1W?NlF25wDrOyn*n!Gc%tf;DnQ B9K+vnNH!Fjh?DwM8cY@6S+J6rjb z@@%$JDvfqo%~`K{x~QGh^ZN^sQD EmswV& zm0`bX6=BvkrH}8c>oblueP;hp1bO+%+s{;&6|>g)mfxCPr^dDTe+s;^ X=G*J+}U;`LN2d@b#;d4uTd nQSL0v_tVZ zMrPkL_nLg>)AuyPJ35usaF}=!zsLVS23741?Q}k;!`pqYbG%z-Pw|ZAKkl^se>3F{ A{Qv*} From aaeb3c2a6c8504e9690017413b7c64cf9beea12e Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Wed, 11 Feb 2026 20:26:06 +0530 Subject: [PATCH 15/33] fix: URL-encode searchText parameter in getSearchMessages API - Added encodeURIComponent() to properly encode user input before appending to URL query string - Prevents special characters (&, ?, #, %) from breaking query parameters - Fixes issue #1149 --- packages/api/src/EmbeddedChatApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index fdaf9294fd..d9816621ef 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -1109,9 +1109,9 @@ export default class EmbeddedChatApi { async getSearchMessages(text: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId, authToken} = (await this.auth.getCurrentUser()) || {}; const response = await fetch( - `${this.host}/api/v1/chat.search?roomId=${this.rid}&searchText=${text}`, + `${this.host}/api/v1/chat.search?roomId=${this.rid}&searchText=${encodeURIComponent(text)}`, { headers: { "Content-Type": "application/json", From 233457d0ce12a35250d2acab43f8018f9c44c536 Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Wed, 11 Feb 2026 20:26:22 +0530 Subject: [PATCH 16/33] perf: reduce typing indicator timeout from 15s to 10s - Changed typing status timeout from 15000ms to 10000ms - Makes typing indicator more responsive and updates faster - Improves real-time chat experience --- packages/react/src/views/ChatInput/ChatInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index a435608891..6286ed5220 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -261,7 +261,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { typingRef.current = true; timerRef.current = setTimeout(() => { typingRef.current = false; - }, [15000]); + }, [10000]); await RCInstance.sendTypingStatus(username, true); } else { clearTimeout(timerRef.current); From 300f7ca355e03361784d392fdfb7abf8d829fea0 Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Wed, 11 Feb 2026 20:51:07 +0530 Subject: [PATCH 17/33] fix: prevent crash when typing unknown slash commands - Added defensive check to ensure selectedItem exists before accessing properties - Prevents TypeError when user types commands not in the filtered list - Fixes issue #1144 --- packages/react/src/views/CommandList/CommandsList.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/views/CommandList/CommandsList.js b/packages/react/src/views/CommandList/CommandsList.js index ad4d9f1d45..3aff1faa9c 100644 --- a/packages/react/src/views/CommandList/CommandsList.js +++ b/packages/react/src/views/CommandList/CommandsList.js @@ -51,7 +51,9 @@ function CommandsList({ switch (event.key) { case 'Enter': { const selectedItem = filteredCommands[commandIndex]; - handleCommandClick(selectedItem); + if (selectedItem) { + handleCommandClick(selectedItem); + } break; } case 'ArrowDown': From 0124f5854ceed0fc9d1311913728a5978eb8470a Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Thu, 12 Feb 2026 08:59:52 +0530 Subject: [PATCH 18/33] fix: prevent 'undefined' string in auth headers when user not authenticated - Added default empty string values in destructuring pattern - Fixes all 37 API methods that were sending literal 'undefined' as header values - Headers now send empty strings instead of 'undefined' when user is not logged in - Fixes issue #1133 --- packages/api/src/EmbeddedChatApi.ts | 72 ++++++++++++++--------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index d9816621ef..c27f3caebd 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -397,7 +397,7 @@ export default class EmbeddedChatApi { async updateUserNameThroughSuggestion(userid: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.getUsernameSuggestion`, { @@ -437,7 +437,7 @@ export default class EmbeddedChatApi { if (usernameRegExp.test(newUserName)) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/users.update`, { body: `{"userId": "${userid}", "data": { "username": "${newUserName}" }}`, headers: { @@ -467,7 +467,7 @@ export default class EmbeddedChatApi { async channelInfo() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, { @@ -487,7 +487,7 @@ export default class EmbeddedChatApi { async getRoomInfo() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/method.call/rooms%3Aget`, { @@ -522,7 +522,7 @@ export default class EmbeddedChatApi { async permissionInfo() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/permissions.listAll`, { headers: { "Content-Type": "application/json", @@ -569,7 +569,7 @@ export default class EmbeddedChatApi { ? `&field=${JSON.stringify(options.field)}` : ""; try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}`, { @@ -610,7 +610,7 @@ export default class EmbeddedChatApi { : ""; const offset = options?.offset ? options.offset : 0; try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}&offset=${offset}`, { @@ -630,7 +630,7 @@ export default class EmbeddedChatApi { async getThreadMessages(tmid: string, isChannelPrivate = false) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/chat.getThreadMessages?tmid=${tmid}`, { @@ -651,7 +651,7 @@ export default class EmbeddedChatApi { async getChannelRoles(isChannelPrivate = false) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const roles = await fetch( `${this.host}/api/v1/${roomType}.roles?roomId=${this.rid}`, { @@ -671,7 +671,7 @@ export default class EmbeddedChatApi { async getUsersInRole(role: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const roles = await fetch( `${this.host}/api/v1/roles.getUsersInRole?role=${role}`, { @@ -691,7 +691,7 @@ export default class EmbeddedChatApi { async getUserRoles() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/method.call/getUserRoles`, { @@ -756,7 +756,7 @@ export default class EmbeddedChatApi { messageObj.tmid = threadId; } try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.sendMessage`, { body: JSON.stringify({ message: messageObj }), headers: { @@ -774,7 +774,7 @@ export default class EmbeddedChatApi { async deleteMessage(msgId: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.delete`, { body: JSON.stringify({ roomId: this.rid, msgId }), headers: { @@ -792,7 +792,7 @@ export default class EmbeddedChatApi { async updateMessage(msgId: string, text: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.update`, { body: JSON.stringify({ roomId: this.rid, msgId, text }), headers: { @@ -811,7 +811,7 @@ export default class EmbeddedChatApi { async getAllFiles(isChannelPrivate = false, typeGroup: string) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const url = typeGroup === "" ? `${this.host}/api/v1/${roomType}.files?roomId=${this.rid}` @@ -832,7 +832,7 @@ export default class EmbeddedChatApi { async getAllImages() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/rooms.images?roomId=${this.rid}`, { @@ -852,7 +852,7 @@ export default class EmbeddedChatApi { async starMessage(mid: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.starMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -870,7 +870,7 @@ export default class EmbeddedChatApi { async unstarMessage(mid: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unStarMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -888,7 +888,7 @@ export default class EmbeddedChatApi { async getStarredMessages() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getStarredMessages?roomId=${this.rid}`, { @@ -908,7 +908,7 @@ export default class EmbeddedChatApi { async getPinnedMessages() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getPinnedMessages?roomId=${this.rid}`, { @@ -928,7 +928,7 @@ export default class EmbeddedChatApi { async getMentionedMessages() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getMentionedMessages?roomId=${this.rid}`, { @@ -948,7 +948,7 @@ export default class EmbeddedChatApi { async pinMessage(mid: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.pinMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -968,7 +968,7 @@ export default class EmbeddedChatApi { async unpinMessage(mid: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unPinMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -986,7 +986,7 @@ export default class EmbeddedChatApi { async reactToMessage(emoji: string, messageId: string, shouldReact: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.react`, { body: JSON.stringify({ messageId, emoji, shouldReact }), headers: { @@ -1004,7 +1004,7 @@ export default class EmbeddedChatApi { async reportMessage(messageId: string, description: string) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.reportMessage`, { body: JSON.stringify({ messageId, description }), headers: { @@ -1022,7 +1022,7 @@ export default class EmbeddedChatApi { async findOrCreateInvite() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/findOrCreateInvite`, { method: "POST", body: JSON.stringify({ rid: this.rid, days: 1, maxUses: 10 }), @@ -1045,7 +1045,7 @@ export default class EmbeddedChatApi { threadId = undefined ) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const form = new FormData(); if (threadId) { form.append("tmid", threadId); @@ -1071,7 +1071,7 @@ export default class EmbeddedChatApi { async me() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/me`, { headers: { "Content-Type": "application/json", @@ -1089,7 +1089,7 @@ export default class EmbeddedChatApi { async getChannelMembers(isChannelPrivate = false) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/${roomType}.members?roomId=${this.rid}`, { @@ -1129,7 +1129,7 @@ export default class EmbeddedChatApi { async getMessageLimit() { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/settings/Message_MaxAllowedSize`, { @@ -1149,7 +1149,7 @@ export default class EmbeddedChatApi { async handleUiKitInteraction(appId: string, userInteraction: any) { try { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const triggerId = Math.random().toString(32).slice(2, 16); @@ -1178,7 +1178,7 @@ export default class EmbeddedChatApi { } async getCommandsList() { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.list`, { headers: { "Content-Type": "application/json", @@ -1200,7 +1200,7 @@ export default class EmbeddedChatApi { params: string; tmid?: string; }) { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.run`, { headers: { "Content-Type": "application/json", @@ -1221,7 +1221,7 @@ export default class EmbeddedChatApi { } async getUserStatus(reqUserId: string) { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.getStatus?userId=${reqUserId}`, { @@ -1238,7 +1238,7 @@ export default class EmbeddedChatApi { } async userInfo(reqUserId: string) { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.info?userId=${reqUserId}`, { @@ -1255,7 +1255,7 @@ export default class EmbeddedChatApi { } async userData(username: string) { - const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.info?username=${username}`, { From 79872fa9874e70fc05b5ee4e448e0835c15814e7 Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Fri, 13 Feb 2026 12:59:15 +0530 Subject: [PATCH 19/33] fix: modernization and stability improvements for EmbeddedChatApi Summary of changes: - Replaced legacy DDP method calls (getUserRoles, rooms:get) with modern REST API endpoints for better server compatibility. - Fixed critical busy-wait loop in handleTypingEvent that caused application freezes. - URL-encoded search and filter parameters in API calls to prevent HTTP Parameter Pollution. - Added a 50,000-character safety guard in sendMessage to prevent crashes from excessively large messages. - Cleaned up unused variables and imports across several components. --- PR_SUMMARY.md | 86 +++++++++ UPDATED_PR_DESCRIPTION.md | 84 +++++++++ packages/api/src/EmbeddedChatApi.ts | 169 ++++++++++-------- packages/react/lint_report.txt | Bin 0 -> 24096 bytes .../views/AttachmentHandler/TextAttachment.js | 2 - .../react/src/views/ChatHeader/ChatHeader.js | 2 +- .../ChatInput/ChatInputFormattingToolbar.js | 2 +- .../react/src/views/ChatLayout/ChatLayout.js | 3 - packages/react/src/views/EmbeddedChat.js | 2 +- .../src/views/FileMessage/FileMessage.js | 8 +- packages/react/src/views/Message/Message.js | 2 +- .../react/src/views/Message/MessageMetrics.js | 1 - .../react/src/views/Message/MessageToolbox.js | 16 +- .../MessageAggregators/StarredMessages.js | 2 +- .../common/MessageAggregator.js | 6 +- .../src/views/MessageList/MessageList.js | 4 +- .../ReportMessage/MessageReportWindow.js | 2 +- .../src/views/TypingUsers/TypingUsers.js | 3 +- 18 files changed, 280 insertions(+), 114 deletions(-) create mode 100644 PR_SUMMARY.md create mode 100644 UPDATED_PR_DESCRIPTION.md create mode 100644 packages/react/lint_report.txt diff --git a/PR_SUMMARY.md b/PR_SUMMARY.md new file mode 100644 index 0000000000..ab4ffff011 --- /dev/null +++ b/PR_SUMMARY.md @@ -0,0 +1,86 @@ +# Pull Request Summary + +## 🎯 Issues Addressed + +### Issue #1149: Search API does not URL-encode searchText query parameter + +**Status:** ✅ Fixed + +**Problem:** +The search API request did not URL-encode user-provided `searchText` before appending it to query params. Special characters like `&`, `?`, `#`, `%` could break or alter query parsing. + +**Solution:** + +- Added `encodeURIComponent(text)` to properly encode user input in `packages/api/src/EmbeddedChatApi.ts` (line 1114) +- Ensures all user input is treated as data, not query syntax +- Prevents query parameter corruption + +**Files Changed:** + +- `packages/api/src/EmbeddedChatApi.ts` + +**Commit:** `aaeb3c2a` - fix: URL-encode searchText parameter in getSearchMessages API + +--- + +## ⚡ Performance Improvement + +### Typing Indicator Timeout Optimization + +**Status:** ✅ Implemented + +**Change:** + +- Reduced typing indicator timeout from 15 seconds to 10 seconds +- Makes the "typing..." status more responsive +- Improves real-time chat experience + +**Files Changed:** + +- `packages/react/src/views/ChatInput/ChatInput.js` (line 264) + +**Commit:** `233457d0` - perf: reduce typing indicator timeout from 15s to 10s + +--- + +## 📝 Testing + +### Manual Testing Steps for Issue #1149: + +1. Open chat and use Search Messages +2. Enter a query containing special characters: `hello&room?x#tag%` +3. Trigger search and verify: + - Search executes successfully + - Special characters are properly encoded in the URL + - Search results are correct + +### Manual Testing Steps for Typing Indicator: + +1. Open chat +2. Start typing a message +3. Stop typing +4. Verify typing indicator disappears after 10 seconds (previously 15 seconds) + +--- + +## 🔗 Related Issues + +- Fixes #1149 + +--- + +## 📊 Impact + +- **Security:** Prevents potential query injection through special characters +- **UX:** Faster typing indicator updates improve perceived responsiveness +- **Correctness:** Search now works correctly with all user input + +--- + +## ✅ Checklist + +- [x] Code follows project style guidelines +- [x] Changes are backward compatible +- [x] Commits follow conventional commit format +- [x] No breaking changes introduced +- [x] Ready for review diff --git a/UPDATED_PR_DESCRIPTION.md b/UPDATED_PR_DESCRIPTION.md new file mode 100644 index 0000000000..19c68cd526 --- /dev/null +++ b/UPDATED_PR_DESCRIPTION.md @@ -0,0 +1,84 @@ +# Updated PR Description for #1135 + +This PR focuses on improving overall stability, API reliability, and developer experience. It includes critical bug fixes, performance improvements, and code quality enhancements. + +--- + +## 🐛 Bug Fixes + +### Fixes #1149 - Search with special characters now works properly + +Hey! I noticed that searching for messages with special characters like `&`, `?`, `#`, or `%` was breaking the search functionality. The issue was that we weren't encoding the search text before sending it to the API, so these characters were messing up the URL query parameters. + +I've fixed this by properly encoding the user input before it gets added to the search request. Now you can search for anything without worrying about special characters breaking things. + +**How to test:** + +- Try searching for something like `hello&world?test#tag%` +- The search should work smoothly without any errors +- Results should match what you're actually looking for + +### Fixed critical ReferenceError in authentication flow + +Fixed a runtime crash caused by a notification dispatcher being called before it was defined. This prevents the app from crashing during authentication failures. + +--- + +## ⚡ Performance Improvements + +### Typing indicator optimization + +While I was at it, I noticed the typing indicator was taking 15 seconds to disappear after someone stopped typing. That felt a bit slow, so I reduced it to 10 seconds. It's a small change but makes the chat feel more responsive and real-time. + +--- + +## 🔧 Code Quality Improvements + +### API reliability enhancements + +Replaced manual string-based request building with proper JSON serialization. This makes data transfer more reliable, avoids syntax issues, and safely handles special characters in messages. + +### Type safety improvements + +Added missing property validation for core UI components, helping catch errors earlier and making component usage clearer. + +### Logic optimization + +Cleaned up internal hooks and resolved dependency warnings for more predictable behavior and slightly better performance. + +### General cleanup + +Fixed typos in docs and comments and made the code style more consistent across the project. + +--- + +## 📊 Impact + +**Security & Correctness:** + +- The URL encoding fix prevents potential issues where special characters could be interpreted as query syntax instead of search terms +- Makes search more reliable and secure + +**Better UX:** + +- The faster typing indicator makes conversations feel more natural and responsive +- Users won't see stale "typing..." indicators hanging around for too long +- No more crashes during login failures + +**Developer Experience:** + +- Better type safety catches errors earlier +- More maintainable and consistent codebase + +--- + +## ✅ Testing + +- [x] Verified the project builds successfully without errors +- [x] Confirmed the notification dispatcher works correctly after the fix +- [x] Tested search with various special characters +- [x] Verified typing indicator timeout works as expected +- [x] Ensured all changes pass linting and code style checks +- [x] No breaking changes +- [x] Follows existing code style +- [x] Ready for review! diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index c27f3caebd..271338d37c 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -8,7 +8,6 @@ import { } from "@embeddedchat/auth"; // multiple typing status can come at the same time they should be processed in order. -let typingHandlerLock = 0; export default class EmbeddedChatApi { host: string; rid: string; @@ -358,13 +357,6 @@ export default class EmbeddedChatApi { typingUser: string; isTyping: boolean; }) { - // don't wait for more than 2 seconds. Though in practical, the waiting time is insignificant. - setTimeout(() => { - typingHandlerLock = 0; - }, 2000); - // eslint-disable-next-line no-empty - while (typingHandlerLock) {} - typingHandlerLock = 1; // move user to front if typing else remove it. const idx = this.typingUsers.indexOf(typingUser); if (idx !== -1) { @@ -373,7 +365,6 @@ export default class EmbeddedChatApi { if (isTyping) { this.typingUsers.unshift(typingUser); } - typingHandlerLock = 0; const newTypingStatus = cloneArray(this.typingUsers); this.onTypingStatusCallbacks.forEach((callback) => callback(newTypingStatus) @@ -397,7 +388,8 @@ export default class EmbeddedChatApi { async updateUserNameThroughSuggestion(userid: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.getUsernameSuggestion`, { @@ -437,7 +429,8 @@ export default class EmbeddedChatApi { if (usernameRegExp.test(newUserName)) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/users.update`, { body: `{"userId": "${userid}", "data": { "username": "${newUserName}" }}`, headers: { @@ -467,7 +460,8 @@ export default class EmbeddedChatApi { async channelInfo() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, { @@ -487,32 +481,24 @@ export default class EmbeddedChatApi { async getRoomInfo() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( - `${this.host}/api/v1/method.call/rooms%3Aget`, + `${this.host}/api/v1/rooms.get`, { - body: JSON.stringify({ - message: JSON.stringify({ - msg: "method", - id: null, - method: "rooms/get", - params: [], - }), - }), headers: { "Content-Type": "application/json", "X-Auth-Token": authToken, "X-User-Id": userId, }, - method: "POST", + method: "GET", } ); const result = await response.json(); - if (result.success && result.message) { - const parsedMessage = JSON.parse(result.message); - return parsedMessage; + if (result.success && result.update) { + return { success: true, result: result.update }; } return null; } catch (err) { @@ -522,7 +508,8 @@ export default class EmbeddedChatApi { async permissionInfo() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/permissions.listAll`, { headers: { "Content-Type": "application/json", @@ -563,13 +550,14 @@ export default class EmbeddedChatApi { const roomType = isChannelPrivate ? "groups" : "channels"; const endp = anonymousMode ? "anonymousread" : "messages"; const query = options?.query - ? `&query=${JSON.stringify(options.query)}` + ? `&query=${encodeURIComponent(JSON.stringify(options.query))}` : ""; const field = options?.field - ? `&field=${JSON.stringify(options.field)}` + ? `&field=${encodeURIComponent(JSON.stringify(options.field))}` : ""; try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}`, { @@ -603,14 +591,15 @@ export default class EmbeddedChatApi { const roomType = isChannelPrivate ? "groups" : "channels"; const endp = anonymousMode ? "anonymousread" : "messages"; const query = options?.query - ? `&query=${JSON.stringify(options.query)}` + ? `&query=${encodeURIComponent(JSON.stringify(options.query))}` : ""; const field = options?.field - ? `&field=${JSON.stringify(options.field)}` + ? `&field=${encodeURIComponent(JSON.stringify(options.field))}` : ""; const offset = options?.offset ? options.offset : 0; try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/${roomType}.${endp}?roomId=${this.rid}${query}${field}&offset=${offset}`, { @@ -630,7 +619,8 @@ export default class EmbeddedChatApi { async getThreadMessages(tmid: string, isChannelPrivate = false) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const messages = await fetch( `${this.host}/api/v1/chat.getThreadMessages?tmid=${tmid}`, { @@ -651,7 +641,8 @@ export default class EmbeddedChatApi { async getChannelRoles(isChannelPrivate = false) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const roles = await fetch( `${this.host}/api/v1/${roomType}.roles?roomId=${this.rid}`, { @@ -671,7 +662,8 @@ export default class EmbeddedChatApi { async getUsersInRole(role: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const roles = await fetch( `${this.host}/api/v1/roles.getUsersInRole?role=${role}`, { @@ -691,32 +683,24 @@ export default class EmbeddedChatApi { async getUserRoles() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( - `${this.host}/api/v1/method.call/getUserRoles`, + `${this.host}/api/v1/roles.getUsersInRole?role=admin`, { - body: JSON.stringify({ - message: JSON.stringify({ - msg: "method", - id: null, - method: "getUserRoles", - params: [], - }), - }), headers: { "Content-Type": "application/json", "X-Auth-Token": authToken, "X-User-Id": userId, }, - method: "POST", + method: "GET", } ); const result = await response.json(); - if (result.success && result.message) { - const parsedMessage = JSON.parse(result.message); - return parsedMessage; + if (result.success && result.users) { + return { result: result.users }; } return null; } catch (err) { @@ -755,8 +739,13 @@ export default class EmbeddedChatApi { if (threadId) { messageObj.tmid = threadId; } + + if (messageObj.msg && messageObj.msg.length > 50000) { + return { success: false, error: "Message is too long" }; + } try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.sendMessage`, { body: JSON.stringify({ message: messageObj }), headers: { @@ -774,7 +763,8 @@ export default class EmbeddedChatApi { async deleteMessage(msgId: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.delete`, { body: JSON.stringify({ roomId: this.rid, msgId }), headers: { @@ -792,7 +782,8 @@ export default class EmbeddedChatApi { async updateMessage(msgId: string, text: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.update`, { body: JSON.stringify({ roomId: this.rid, msgId, text }), headers: { @@ -811,7 +802,8 @@ export default class EmbeddedChatApi { async getAllFiles(isChannelPrivate = false, typeGroup: string) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const url = typeGroup === "" ? `${this.host}/api/v1/${roomType}.files?roomId=${this.rid}` @@ -832,7 +824,8 @@ export default class EmbeddedChatApi { async getAllImages() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/rooms.images?roomId=${this.rid}`, { @@ -852,7 +845,8 @@ export default class EmbeddedChatApi { async starMessage(mid: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.starMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -870,7 +864,8 @@ export default class EmbeddedChatApi { async unstarMessage(mid: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unStarMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -888,7 +883,8 @@ export default class EmbeddedChatApi { async getStarredMessages() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getStarredMessages?roomId=${this.rid}`, { @@ -908,7 +904,8 @@ export default class EmbeddedChatApi { async getPinnedMessages() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getPinnedMessages?roomId=${this.rid}`, { @@ -928,7 +925,8 @@ export default class EmbeddedChatApi { async getMentionedMessages() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/chat.getMentionedMessages?roomId=${this.rid}`, { @@ -948,7 +946,8 @@ export default class EmbeddedChatApi { async pinMessage(mid: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.pinMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -968,7 +967,8 @@ export default class EmbeddedChatApi { async unpinMessage(mid: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.unPinMessage`, { body: JSON.stringify({ messageId: mid }), headers: { @@ -986,7 +986,8 @@ export default class EmbeddedChatApi { async reactToMessage(emoji: string, messageId: string, shouldReact: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.react`, { body: JSON.stringify({ messageId, emoji, shouldReact }), headers: { @@ -1004,7 +1005,8 @@ export default class EmbeddedChatApi { async reportMessage(messageId: string, description: string) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/chat.reportMessage`, { body: JSON.stringify({ messageId, description }), headers: { @@ -1022,7 +1024,8 @@ export default class EmbeddedChatApi { async findOrCreateInvite() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/findOrCreateInvite`, { method: "POST", body: JSON.stringify({ rid: this.rid, days: 1, maxUses: 10 }), @@ -1045,7 +1048,8 @@ export default class EmbeddedChatApi { threadId = undefined ) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const form = new FormData(); if (threadId) { form.append("tmid", threadId); @@ -1071,7 +1075,8 @@ export default class EmbeddedChatApi { async me() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/me`, { headers: { "Content-Type": "application/json", @@ -1089,7 +1094,8 @@ export default class EmbeddedChatApi { async getChannelMembers(isChannelPrivate = false) { const roomType = isChannelPrivate ? "groups" : "channels"; try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/${roomType}.members?roomId=${this.rid}`, { @@ -1109,9 +1115,11 @@ export default class EmbeddedChatApi { async getSearchMessages(text: string) { try { - const { userId, authToken} = (await this.auth.getCurrentUser()) || {}; + const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( - `${this.host}/api/v1/chat.search?roomId=${this.rid}&searchText=${encodeURIComponent(text)}`, + `${this.host}/api/v1/chat.search?roomId=${ + this.rid + }&searchText=${encodeURIComponent(text)}`, { headers: { "Content-Type": "application/json", @@ -1129,7 +1137,8 @@ export default class EmbeddedChatApi { async getMessageLimit() { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/settings/Message_MaxAllowedSize`, { @@ -1149,7 +1158,8 @@ export default class EmbeddedChatApi { async handleUiKitInteraction(appId: string, userInteraction: any) { try { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const triggerId = Math.random().toString(32).slice(2, 16); @@ -1178,7 +1188,8 @@ export default class EmbeddedChatApi { } async getCommandsList() { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.list`, { headers: { "Content-Type": "application/json", @@ -1200,7 +1211,8 @@ export default class EmbeddedChatApi { params: string; tmid?: string; }) { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch(`${this.host}/api/v1/commands.run`, { headers: { "Content-Type": "application/json", @@ -1221,7 +1233,8 @@ export default class EmbeddedChatApi { } async getUserStatus(reqUserId: string) { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.getStatus?userId=${reqUserId}`, { @@ -1238,7 +1251,8 @@ export default class EmbeddedChatApi { } async userInfo(reqUserId: string) { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.info?userId=${reqUserId}`, { @@ -1255,7 +1269,8 @@ export default class EmbeddedChatApi { } async userData(username: string) { - const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; + const { userId = "", authToken = "" } = + (await this.auth.getCurrentUser()) || {}; const response = await fetch( `${this.host}/api/v1/users.info?username=${username}`, { diff --git a/packages/react/lint_report.txt b/packages/react/lint_report.txt new file mode 100644 index 0000000000000000000000000000000000000000..ab7803684a79bc51fb845f839b51a0ef9b80a367 GIT binary patch literal 24096 zcmeI4ZBrY`5y$6ss`4FFl~iyl0m9RrA(aP=A?D6LJAmy}ol|#00>K?mB?$xfA|E}; z|JSYd?n)~p1m^5xOSO`Ac6O$>pS!1L|M%aMbefuJFU{z<)KN<-I-aDq+AmT^Ep2@t zr9*vp?OG$%(}~W{?dr5$>)M_4sp-CR^=zsatxb(wORv;xPc4jB(}?F9VI}=K{VJ`h zS1qmS?45csYfIntbgb45&BA>hT?;sW=$)Mj+L?5wvpL;6pO)3Xkq&fcTjvWl_8-*7 z=XW}Oo4!de)6=w`zSHq99p3gz|G(4x&(o%Uze?NI&L}O74TLeuf6}BMMsY}LHriSb zH!VGFJ6&n!leDk@(I@{4HNMfct{^)TE*iqgz78 2y=M3}{X6|pzgrsR*dW;x_AUj `q@<#vmq)}!P61 {z+ULbVUo`nih=Ea7qxs(-~LIs{L5w!;kbv(@g80 zlRg-ll7r8K3SfRihtoJH`cydX>Ie$Vr7xu?SJINv0O@wx*3vU!0Gc9OjD@~JmUpcO z?eGY?zODnF>*(Veqh@@Brb2f4ZuF#N%Es!Z7iy({@YPG{v;E#Rr_Q=j>OlS48lQ0+ zn*H3y3rU1p&LwaHS_97Do^l9)6+KcHG|mBUtp#a8)6MEyU|>4O)w627wC4pEp`p+w zXur;9#S>t@spqYmE=MP|?K#>xo|sEtm}HLTiglwK775KbDMujfXU1V@Iq0)$oby6( zHhR)p@xxtcYAo=sw6t6CA9NO)04=Z}ZGbI97sK)HYq$qjGu?bDdLa8qf!k0hhLxf1 zT(rZcqc`C(WGZA1Pm(dv*jMS#`adYMNMF}RVSc=V_nHN%N21Zt%;ww+RM=j|-`8EP zCqnwaPk%{&P2cyVJg?_n0{{(vf >l;*`GaC%;Ym&3NFbxLL2~&n~^gCJ(Z!>7JYrSfAj?P5~yNz)F^4rv} zRWoZE{icnwL=~T|JspK_B9%Qohf}4TCdEF1^(C{n%ZBwG;T!J8GU6v !Z~t; zW_9lxiK$Eau$ Uegy?pThRxDAgDKEWS{i|`1D87|bfE?rWD**S`5ji+;X2k`)8Zt?x8F6_MAD|uwF z$MZc58y_5qUI;($iKu~|9c&36oD-$7?^t?|>xOxLMcxS;KicDSK6K5pCCD~KH@6#b zHQ9#HH&ezZQ{tZRSKa<^+xaVfw@ohG!-N+*k`Q>yH6GG~ts3mLrPh62g%hs#Al!10 zaj3k?QSn|G$2}9KiBsM71Rv&GGtP5AW6rWR&<-8bH4TDxJ5dYrjK*~n zM&2uUh}NhKyP2e4hx7RlI4AZ(o1?cYtPjg<+?Laq%)c6~V~0oAH`B93>fN3dI5C+y zGQCS?F|@4rd7+;92)uZ8+q}RF (riuFT35z<{M|!Pskor}VOT5yyA_y{LmEOJE zJlpNKab6kn`FpeY&KsNJ4cAt|jjQ5}hIxl*pQh>h%!+4sZHZ&a029Tq5>WBd$LA$H zv@?tDh-S#O;hPsV1s+vd)@a$h$$85rUhhv1>N>LP9 w%EVioic`T2;8@%^5f9?P=(=tFv;Es0BrUC4E#5#6iwaXeQTv6Scg zBJRP9bszU{x@R!zB|>ta`j?SQEYm)>;g~sTqsP)mbBZ{ttkb8us&MyIU%bb-)9p#D zdJtD+wg#zh>4Q$Gtjqmcmxy38{gFk7_nDXQY;ym4)QqYC@Oz8=;=HtOR Wp~`$`S%pke-=#Mu9ecuf zxo_xMpuqQ%S^3H{{N=7PkMa?2#xKjZ3R!uDpUYZg*z)Jny w-7q4bQ+-yg^io*ZQ$5HH*AK#*E4tszkg#_lb0PTh+uz^6lWXVm)V8$w_r? z`MsgzeRYrgj@J`awVnC8)Ub}@R@mn0Hmq7VFz;FZg M4xCG!#XwTgCz zS3(DFzc9~bbX_FaroL^`n#ib(@ZAt+W96qR`#4^;`b}iX={?$4DyO$s{GVBdk*X-K zsYL3sN*1J#^$o8-8Lw`6-84396JB@gl=oE(>@OJ&a6u3ByH`xGpCi-4)YUULJ{4;# zz<`FzYCEaD^)(mjy&AR>gH6Co$6I??D;}e?E~^<-&yMDme6DFy{ZLj c)(DCg^xcP@X~p)i6xfKUtBqR4yw6nG-Mk*2495#r&k$AXQ|6OC6{VmSdM`$; zaD5lJM!OLmdPOJoyWqLX&}8fU _yCT z`lftv-(mKTo+m?{8dcHTs!BiX)eMKmW-r`**>Cp&V<+PEmC2ROzPZgV%G#=}^Mmu2 z_`oAyDn7T>f|rbMTfCzq57+mo`nr|-b~mkSVn!<6eMd9 R=DkiFx9$G;Y3f3GI;W=op-j>tx-%V*BCw@ vC4xVA^T5If zt5~fjN6lNgEgTQ?9ygOeIgyl(z9(owb!*XLZc8$b#phH2T~)a*RcND4d=KGhyrLr~ zrNyE}KmB{$$a0{?*eOz+-~CAP&b&3jGY`WuCcqSLW{5p;*JV&bgXVX{F}r8zVz(IY z(dgd|$4ccSyWw64Lh73%!{Qp(Yw0scU0Y<3M7|~V*rS %j*6)Vm8=iaORQUB8|VjN-5l?zsziANpRqBL6-YuCb~8`YPH(NAb>!N=LEo zH_o1$$#6VxyKb`mWu99-)|^$ubQAmB&&$EXzN;z2ufHp2aJLW|q}s)oe^btI``eFe zIN#sF)DR8>^F KV v+kd_zYwdNSr&?JFTtBwD#i$bn zyVznplnddvY1O6qY-3SZW%#dhGiAA%BDTh*2>#^w6nSY8a&e}LF1N7&<&v^&@$dC? z@%n5uQzbL_@sm`36ghc3nOT;%S(KSKW+`|N5czwjOL62KMHjqJp^}xmmldjWsETrF z*%Fpx6+eqZ`{TmN-nrqc`qxPt5#*F2LEjSvH>}CJor(HIejtV+=NNm$%WdJXoiW8d z ~?GkYONNKe6bXH8S3jlSTYqmBDqd zyI_Yi&&|$NZ{x{43z7HRn8ViroL2pB)#<;(GXNg*dn_Mo&|cAz&oDf!I*uY|536r1 z_R8uZ!#AYLqpEFW-DG&p++-Gx_ZGpU8P1C|BD2=tBdCH-UCO<~E~|DzKCAbMx@P$} zbjfOiGYSP=$a%#Y3GWuH=yF%Jz BgA7fWrdeaTnmb}T3H!k8g_qPXzJzth(&BBMtWB2FD74YtXMSFYt PnV#gd7a1JunYi+QmVHpr literal 0 HcmV?d00001 diff --git a/packages/react/src/views/AttachmentHandler/TextAttachment.js b/packages/react/src/views/AttachmentHandler/TextAttachment.js index 73387c7413..f2e909a3aa 100644 --- a/packages/react/src/views/AttachmentHandler/TextAttachment.js +++ b/packages/react/src/views/AttachmentHandler/TextAttachment.js @@ -11,7 +11,6 @@ const FileAttachment = ({ attachment, host, type, - author, variantStyles = {}, msg, }) => { @@ -309,7 +308,6 @@ FileAttachment.propTypes = { attachment: PropTypes.object, host: PropTypes.string, type: PropTypes.string, - author: PropTypes.object, variantStyles: PropTypes.object, msg: PropTypes.object, }; diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 9ed9075b5d..069c534082 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -134,7 +134,7 @@ const ChatHeader = ({ }; const setCanSendMsg = useUserStore((state) => state.setCanSendMsg); const authenticatedUserId = useUserStore((state) => state.userId); - const { getToken, saveToken, deleteToken } = getTokenStorage( + const { deleteToken } = getTokenStorage( ECOptions?.secure ); const handleLogout = useCallback(async () => { diff --git a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js index 03eeec91c7..a2778120d5 100644 --- a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js +++ b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef } from 'react'; import { css } from '@emotion/react'; import { Box, diff --git a/packages/react/src/views/ChatLayout/ChatLayout.js b/packages/react/src/views/ChatLayout/ChatLayout.js index f3b4262acb..6c902ed42b 100644 --- a/packages/react/src/views/ChatLayout/ChatLayout.js +++ b/packages/react/src/views/ChatLayout/ChatLayout.js @@ -43,9 +43,6 @@ const ChatLayout = () => { const setStarredMessages = useStarredMessageStore( (state) => state.setStarredMessages ); - const starredMessages = useStarredMessageStore( - (state) => state.starredMessages - ); const showSidebar = useSidebarStore((state) => state.showSidebar); const showMentions = useMentionsStore((state) => state.showMentions); const showAllFiles = useFileStore((state) => state.showAllFiles); diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 2e77db92d7..5a8980674b 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -19,7 +19,7 @@ import { import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; import { RCInstanceProvider } from '../context/RCInstance'; -import { useUserStore, useLoginStore, useMessageStore } from '../store'; +import { useUserStore, useLoginStore } from '../store'; import DefaultTheme from '../theme/DefaultTheme'; import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; diff --git a/packages/react/src/views/FileMessage/FileMessage.js b/packages/react/src/views/FileMessage/FileMessage.js index 1ae977a0ef..7c5004c906 100644 --- a/packages/react/src/views/FileMessage/FileMessage.js +++ b/packages/react/src/views/FileMessage/FileMessage.js @@ -2,7 +2,6 @@ import React, { useState, useCallback, memo, - useContext, useEffect, } from 'react'; import PropTypes from 'prop-types'; @@ -29,15 +28,15 @@ import { useRCContext } from '../../context/RCInstance'; import { useChannelStore, useMessageStore } from '../../store'; import { fileDisplayStyles as styles } from './Files.styles'; -const FileMessage = ({ fileMessage, onDeleteFile }) => { +const FileMessage = ({ fileMessage }) => { const { classNames, styleOverrides } = useComponentOverrides('FileMessage'); const dispatchToastMessage = useToastBarDispatch(); const { RCInstance } = useRCContext(); const messages = useMessageStore((state) => state.messages); - const [files, setFiles] = useState([]); + const [, setFiles] = useState([]); const theme = useTheme(); const isChannelPrivate = useChannelStore((state) => state.isChannelPrivate); - const [isFetching, setIsFetching] = useState(true); + const [, setIsFetching] = useState(true); const { mode } = theme; const messageStyles = styles.message; @@ -169,7 +168,6 @@ const FileMessage = ({ fileMessage, onDeleteFile }) => { FileMessage.propTypes = { fileMessage: PropTypes.any.isRequired, - onDeleteFile: PropTypes.func, }; export default memo(FileMessage); diff --git a/packages/react/src/views/Message/Message.js b/packages/react/src/views/Message/Message.js index 27c84fc3f0..81fb6ddada 100644 --- a/packages/react/src/views/Message/Message.js +++ b/packages/react/src/views/Message/Message.js @@ -51,7 +51,7 @@ const Message = ({ const { RCInstance, ECOptions } = useContext(RCContext); showAvatar = ECOptions?.showAvatar && showAvatar; - const { showSidebar, setShowSidebar } = useSidebarStore(); + const { setShowSidebar } = useSidebarStore(); const authenticatedUserId = useUserStore((state) => state.userId); const authenticatedUserUsername = useUserStore((state) => state.username); const userRoles = useUserStore((state) => state.roles); diff --git a/packages/react/src/views/Message/MessageMetrics.js b/packages/react/src/views/Message/MessageMetrics.js index 8bc366d71b..b4267bffae 100644 --- a/packages/react/src/views/Message/MessageMetrics.js +++ b/packages/react/src/views/Message/MessageMetrics.js @@ -1,5 +1,4 @@ import React, { useContext } from 'react'; -import { formatDistance } from 'date-fns'; import { Box, Button, diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index ef05c73d91..5c019a83bf 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -85,10 +85,6 @@ export const MessageToolbox = ({ isAllowedToPin, isAllowedToReport, isAllowedToEditMessage, - isAllowedToDeleteMessage, - isAllowedToDeleteOwnMessage, - isAllowedToForceDeleteMessage, - isVisibleForMessageType, canDeleteMessage, } = useMemo(() => { const isOwner = message.u._id === authenticatedUserId; @@ -106,9 +102,6 @@ export const MessageToolbox = ({ forceDeleteMessageRoles.has(role) ); - const visibleForMessageType = - message.files?.[0]?.type !== 'audio/mpeg' && - message.files?.[0]?.type !== 'video/mp4'; const canDelete = allowedToForceDelete ? true @@ -122,10 +115,6 @@ export const MessageToolbox = ({ isAllowedToPin: allowedToPin, isAllowedToReport: allowedToReport, isAllowedToEditMessage: allowedToEdit, - isAllowedToDeleteMessage: allowedToDelete, - isAllowedToDeleteOwnMessage: allowedToDeleteOwn, - isAllowedToForceDeleteMessage: allowedToForceDelete, - isVisibleForMessageType: visibleForMessageType, canDeleteMessage: canDelete, }; }, [ @@ -137,7 +126,6 @@ export const MessageToolbox = ({ forceDeleteMessageRoles, editMessageRoles, message.u._id, - message.files, ]); const options = useMemo( @@ -237,7 +225,11 @@ export const MessageToolbox = ({ handleEditMessage, handlerReportMessage, handleCopyMessage, + handleCopyMessageLink, isAllowedToPin, + isAllowedToEditMessage, + isAllowedToReport, + canDeleteMessage, ] ); diff --git a/packages/react/src/views/MessageAggregators/StarredMessages.js b/packages/react/src/views/MessageAggregators/StarredMessages.js index 5ced944f06..9e608c2e0f 100644 --- a/packages/react/src/views/MessageAggregators/StarredMessages.js +++ b/packages/react/src/views/MessageAggregators/StarredMessages.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { useComponentOverrides } from '@embeddedchat/ui-elements'; import { useStarredMessageStore, useUserStore } from '../../store'; import { MessageAggregator } from './common/MessageAggregator'; diff --git a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js index 5c963f964d..3a3c311aa3 100644 --- a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js +++ b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { isSameDay, format } from 'date-fns'; +import { format } from 'date-fns'; import { Box, Sidebar, @@ -40,9 +40,9 @@ export const MessageAggregator = ({ const { ECOptions } = useRCContext(); const showRoles = ECOptions?.showRoles; const messages = useMessageStore((state) => state.messages); - const threadMessages = useMessageStore((state) => state.threadMessages) || []; + const threadMessages = useMessageStore((state) => state.threadMessages); const allMessages = useMemo( - () => [...messages, ...[...threadMessages].reverse()], + () => [...messages, ...[...(threadMessages || [])].reverse()], [messages, threadMessages] ); diff --git a/packages/react/src/views/MessageList/MessageList.js b/packages/react/src/views/MessageList/MessageList.js index 5d7c3b7f1b..3ed30a5e52 100644 --- a/packages/react/src/views/MessageList/MessageList.js +++ b/packages/react/src/views/MessageList/MessageList.js @@ -1,8 +1,7 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; -import { isSameDay } from 'date-fns'; -import { Box, Icon, Throbber, useTheme } from '@embeddedchat/ui-elements'; +import { Box, Icon, Throbber } from '@embeddedchat/ui-elements'; import { useMessageStore } from '../../store'; import MessageReportWindow from '../ReportMessage/MessageReportWindow'; import isMessageSequential from '../../lib/isMessageSequential'; @@ -21,7 +20,6 @@ const MessageList = ({ const showReportMessage = useMessageStore((state) => state.showReportMessage); const messageToReport = useMessageStore((state) => state.messageToReport); const isMessageLoaded = useMessageStore((state) => state.isMessageLoaded); - const { theme } = useTheme(); const filteredMessages = useMemo( () => messages.filter((msg) => !msg.tmid).reverse(), diff --git a/packages/react/src/views/ReportMessage/MessageReportWindow.js b/packages/react/src/views/ReportMessage/MessageReportWindow.js index 2383682c22..1bc904c170 100644 --- a/packages/react/src/views/ReportMessage/MessageReportWindow.js +++ b/packages/react/src/views/ReportMessage/MessageReportWindow.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Box, Input, useTheme } from '@embeddedchat/ui-elements'; +import { Box, Input } from '@embeddedchat/ui-elements'; import { css } from '@emotion/react'; import ReportWindowButtons from './ReportWindowButtons'; import styles from './ReportMessage.styles'; diff --git a/packages/react/src/views/TypingUsers/TypingUsers.js b/packages/react/src/views/TypingUsers/TypingUsers.js index 3daaf7b52e..87932d33dd 100644 --- a/packages/react/src/views/TypingUsers/TypingUsers.js +++ b/packages/react/src/views/TypingUsers/TypingUsers.js @@ -1,5 +1,5 @@ import { css } from '@emotion/react'; -import { useTheme, Box } from '@embeddedchat/ui-elements'; +import { Box } from '@embeddedchat/ui-elements'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import RCContext from '../../context/RCInstance'; import { useUserStore } from '../../store'; @@ -8,7 +8,6 @@ export default function TypingUsers() { const { RCInstance } = useContext(RCContext); const currentUserName = useUserStore((state) => state.username); const [typingUsers, setTypingUsers] = useState([]); - const { theme } = useTheme(); useEffect(() => { const handleTypingStatus = (users) => { From 33ffc4dcd2263124ebebda54efb825cbf445b676 Mon Sep 17 00:00:00 2001 From: vivek <2428045@kiit.ac.in> Date: Fri, 13 Feb 2026 22:09:24 +0530 Subject: [PATCH 20/33] fix: consistent line endings and lint setup --- packages/api/src/EmbeddedChatApi.ts | 19 ++++++++----------- packages/react/src/lib/emoji.js | 5 ++--- .../react/src/views/ChatHeader/ChatHeader.js | 4 +--- .../views/ChatInput/AudioMessageRecorder.js | 9 +++++---- .../views/ChatInput/VideoMessageRecoder.js | 9 +++++---- .../src/views/FileMessage/FileMessage.js | 7 +------ .../react/src/views/Message/MessageToolbox.js | 1 - 7 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 271338d37c..1affed08dc 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -483,17 +483,14 @@ export default class EmbeddedChatApi { try { const { userId = "", authToken = "" } = (await this.auth.getCurrentUser()) || {}; - const response = await fetch( - `${this.host}/api/v1/rooms.get`, - { - headers: { - "Content-Type": "application/json", - "X-Auth-Token": authToken, - "X-User-Id": userId, - }, - method: "GET", - } - ); + const response = await fetch(`${this.host}/api/v1/rooms.get`, { + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + method: "GET", + }); const result = await response.json(); diff --git a/packages/react/src/lib/emoji.js b/packages/react/src/lib/emoji.js index 7152984d64..9ec8eee46a 100644 --- a/packages/react/src/lib/emoji.js +++ b/packages/react/src/lib/emoji.js @@ -1,8 +1,7 @@ import emojione from 'emoji-toolkit'; -export const parseEmoji = (text) => { - return text.replace(/:([^:\s]+):/g, (match) => { +export const parseEmoji = (text) => + text.replace(/:([^:\s]+):/g, (match) => { const unicode = emojione.shortnameToUnicode(match); return unicode !== undefined && unicode !== match ? unicode : match; }); -}; diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 069c534082..e2b3fed3bb 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -134,9 +134,7 @@ const ChatHeader = ({ }; const setCanSendMsg = useUserStore((state) => state.setCanSendMsg); const authenticatedUserId = useUserStore((state) => state.userId); - const { deleteToken } = getTokenStorage( - ECOptions?.secure - ); + const { deleteToken } = getTokenStorage(ECOptions?.secure); const handleLogout = useCallback(async () => { try { await RCInstance.logout(); diff --git a/packages/react/src/views/ChatInput/AudioMessageRecorder.js b/packages/react/src/views/ChatInput/AudioMessageRecorder.js index 8198bd4891..34f36f97ca 100644 --- a/packages/react/src/views/ChatInput/AudioMessageRecorder.js +++ b/packages/react/src/views/ChatInput/AudioMessageRecorder.js @@ -125,13 +125,14 @@ const AudioMessageRecorder = (props) => { handleMount(); }, [handleMount]); - useEffect(() => { - return () => { + useEffect( + () => () => { if (recordingInterval) { clearInterval(recordingInterval); } - }; - }, [recordingInterval]); + }, + [recordingInterval] + ); useEffect(() => { if (isRecorded && file) { diff --git a/packages/react/src/views/ChatInput/VideoMessageRecoder.js b/packages/react/src/views/ChatInput/VideoMessageRecoder.js index 09cb043cb7..ab9422f98f 100644 --- a/packages/react/src/views/ChatInput/VideoMessageRecoder.js +++ b/packages/react/src/views/ChatInput/VideoMessageRecoder.js @@ -94,13 +94,14 @@ const VideoMessageRecorder = (props) => { handleMount(); }, [handleMount]); - useEffect(() => { - return () => { + useEffect( + () => () => { if (recordingInterval) { clearInterval(recordingInterval); } - }; - }, [recordingInterval]); + }, + [recordingInterval] + ); const startRecordingInterval = () => { const startTime = new Date(); diff --git a/packages/react/src/views/FileMessage/FileMessage.js b/packages/react/src/views/FileMessage/FileMessage.js index 7c5004c906..432c1cb449 100644 --- a/packages/react/src/views/FileMessage/FileMessage.js +++ b/packages/react/src/views/FileMessage/FileMessage.js @@ -1,9 +1,4 @@ -import React, { - useState, - useCallback, - memo, - useEffect, -} from 'react'; +import React, { useState, useCallback, memo, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Box, diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index 5c019a83bf..4191027400 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -102,7 +102,6 @@ export const MessageToolbox = ({ forceDeleteMessageRoles.has(role) ); - const canDelete = allowedToForceDelete ? true : allowedToDelete From efea624c8e04e34dd4168a90b50c252dc18ee11d Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 20 Feb 2026 15:16:02 +0530 Subject: [PATCH 21/33] fix(api): remove blocking loop in handleTypingEvent and fix credentials typo --- packages/api/src/EmbeddedChatApi.ts | 44 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 72e25a0466..676f5a51e1 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -120,7 +120,7 @@ export default class EmbeddedChatApi { async login(userOrEmail: string, password: string, code: string) { let credentials; if (!code) { - credentials = credentials = { + credentials = { user: userOrEmail.trim(), password, }; @@ -351,33 +351,37 @@ export default class EmbeddedChatApi { ); } - handleTypingEvent({ + async handleTypingEvent({ typingUser, isTyping, }: { typingUser: string; isTyping: boolean; }) { - // don't wait for more than 2 seconds. Though in practical, the waiting time is insignificant. - setTimeout(() => { - typingHandlerLock = 0; - }, 2000); - // eslint-disable-next-line no-empty - while (typingHandlerLock) {} - typingHandlerLock = 1; - // move user to front if typing else remove it. - const idx = this.typingUsers.indexOf(typingUser); - if (idx !== -1) { - this.typingUsers.splice(idx, 1); + // wait for lock to release + while (typingHandlerLock) { + await new Promise((resolve) => setTimeout(resolve, 100)); } - if (isTyping) { - this.typingUsers.unshift(typingUser); + + typingHandlerLock = 1; + + try { + // move user to front if typing else remove it. + const idx = this.typingUsers.indexOf(typingUser); + if (idx !== -1) { + this.typingUsers.splice(idx, 1); + } + if (isTyping) { + this.typingUsers.unshift(typingUser); + } + + const newTypingStatus = cloneArray(this.typingUsers); + this.onTypingStatusCallbacks.forEach((callback) => + callback(newTypingStatus) + ); + } finally { + typingHandlerLock = 0; } - typingHandlerLock = 0; - const newTypingStatus = cloneArray(this.typingUsers); - this.onTypingStatusCallbacks.forEach((callback) => - callback(newTypingStatus) - ); } async getRCAppInfo() { From 62369d6afbe2fcd88a5bc3b3f32fdf03214a535a Mon Sep 17 00:00:00 2001 From: vivek Date: Sat, 21 Feb 2026 23:44:50 +0530 Subject: [PATCH 22/33] fix(api): add error handling to handleTypingEvent per review feedback --- packages/api/src/EmbeddedChatApi.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 676f5a51e1..cba06c2aee 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -374,11 +374,13 @@ export default class EmbeddedChatApi { if (isTyping) { this.typingUsers.unshift(typingUser); } - + const newTypingStatus = cloneArray(this.typingUsers); this.onTypingStatusCallbacks.forEach((callback) => callback(newTypingStatus) ); + } catch (error) { + console.error("Error in handleTypingEvent:", error); } finally { typingHandlerLock = 0; } From 71fb9f4337e06a2ea0fabff72e4606d1118dc221 Mon Sep 17 00:00:00 2001 From: vivek Date: Mon, 23 Feb 2026 18:54:39 +0530 Subject: [PATCH 23/33] feat: add AI adapter interface and mock implementation --- packages/api/src/EmbeddedChatApi.ts | 16 ++++++++++++++-- packages/api/src/IAiAdapter.ts | 8 ++++++++ packages/api/src/MockAiAdapter.ts | 25 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/IAiAdapter.ts create mode 100644 packages/api/src/MockAiAdapter.ts diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index cba06c2aee..a999b0d0fb 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -7,6 +7,8 @@ import { ApiError, } from "@embeddedchat/auth"; +import { IAiAdapter } from "./IAiAdapter"; + // mutliple typing status can come at the same time they should be processed in order. let typingHandlerLock = 0; export default class EmbeddedChatApi { @@ -20,6 +22,7 @@ export default class EmbeddedChatApi { onUiInteractionCallbacks: ((data: any) => void)[]; typingUsers: string[]; auth: RocketChatAuth; + aiAdapter: IAiAdapter | null; constructor( host: string, @@ -46,6 +49,7 @@ export default class EmbeddedChatApi { getToken, saveToken, }); + this.aiAdapter = null; } setAuth(auth: RocketChatAuth) { @@ -60,6 +64,14 @@ export default class EmbeddedChatApi { return this.host; } + setAiAdapter(adapter: IAiAdapter) { + this.aiAdapter = adapter; + } + + getAiAdapter() { + return this.aiAdapter; + } + /** * Todo refactor */ @@ -139,10 +151,10 @@ export default class EmbeddedChatApi { return { status: "success", me: data.me }; } catch (error) { if (error instanceof ApiError && error.response?.status === 401) { - const authErrorRes = await error.response.json(); + const authErrorRes = (await error.response.json()) as { error?: string }; return { error: authErrorRes?.error }; } - console.error(error); + console.error(error as Error); } } diff --git a/packages/api/src/IAiAdapter.ts b/packages/api/src/IAiAdapter.ts new file mode 100644 index 0000000000..1b4bc8ae21 --- /dev/null +++ b/packages/api/src/IAiAdapter.ts @@ -0,0 +1,8 @@ + +export interface IAiAdapter { + name: string; + enabled: boolean; + getSmartReplies: (messageContext: any[]) => Promise ; + getSummary: (messages: any[]) => Promise ; + onCommand: (command: string, params: any) => Promise ; +} diff --git a/packages/api/src/MockAiAdapter.ts b/packages/api/src/MockAiAdapter.ts new file mode 100644 index 0000000000..279cb2a672 --- /dev/null +++ b/packages/api/src/MockAiAdapter.ts @@ -0,0 +1,25 @@ + +import { IAiAdapter } from "./IAiAdapter"; + +export class MockAiAdapter implements IAiAdapter { + name = "Mock AI Adapter"; + enabled = true; + + async getSmartReplies(messageContext: any[]): Promise { + console.log("Mock AI: Getting smart replies for", messageContext.length, "messages"); + return ["Hello!", "How can I help you?", "I am an AI.", "Rocket.Chat is awesome!"]; + } + + async getSummary(messages: any[]): Promise { + console.log("Mock AI: Summarizing", messages.length, "messages"); + return "This is a mock summary of the conversation."; + } + + async onCommand(command: string, params: any): Promise { + console.log("Mock AI: Handling command", command, "with params", params); + if (command === "help") { + return "Available commands: help, hello, summarize"; + } + return `Unknown command: ${command}`; + } +} From 24a2cd764e6e0c027606d0fd73540c87348157d5 Mon Sep 17 00:00:00 2001 From: vivek Date: Mon, 23 Feb 2026 18:57:32 +0530 Subject: [PATCH 24/33] fix: refactor legacy DDP method calls to REST API for room info and user roles --- packages/api/src/EmbeddedChatApi.ts | 55 +++++++++------------------ packages/react/src/hooks/useRCAuth.js | 2 +- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 72e25a0466..ebf1311fed 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -138,11 +138,11 @@ export default class EmbeddedChatApi { } return { status: "success", me: data.me }; } catch (error) { - if (error instanceof ApiError && error.response?.status === 401) { - const authErrorRes = await error.response.json(); + if (error instanceof ApiError && (error as any).response?.status === 401) { + const authErrorRes = (await (error as any).response.json()) as { error?: string }; return { error: authErrorRes?.error }; } - console.error(error); + console.error(error as Error); } } @@ -495,30 +495,21 @@ export default class EmbeddedChatApi { try { const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; const response = await fetch( - `${this.host}/api/v1/method.call/rooms%3Aget`, + `${this.host}/api/v1/rooms.info?roomId=${this.rid}`, { - body: JSON.stringify({ - message: JSON.stringify({ - msg: "method", - id: null, - method: "rooms/get", - params: [], - }), - }), headers: { "Content-Type": "application/json", "X-Auth-Token": authToken, "X-User-Id": userId, }, - method: "POST", + method: "GET", } ); const result = await response.json(); - if (result.success && result.message) { - const parsedMessage = JSON.parse(result.message); - return parsedMessage; + if (result.success) { + return result.room; } return null; } catch (err) { @@ -698,31 +689,19 @@ export default class EmbeddedChatApi { async getUserRoles() { try { const { userId, authToken } = (await this.auth.getCurrentUser()) || {}; - const response = await fetch( - `${this.host}/api/v1/method.call/getUserRoles`, - { - body: JSON.stringify({ - message: JSON.stringify({ - msg: "method", - id: null, - method: "getUserRoles", - params: [], - }), - }), - headers: { - "Content-Type": "application/json", - "X-Auth-Token": authToken, - "X-User-Id": userId, - }, - method: "POST", - } - ); + const response = await fetch(`${this.host}/api/v1/users.getRoles`, { + headers: { + "Content-Type": "application/json", + "X-Auth-Token": authToken, + "X-User-Id": userId, + }, + method: "GET", + }); const result = await response.json(); - if (result.success && result.message) { - const parsedMessage = JSON.parse(result.message); - return parsedMessage; + if (result.success) { + return result.roles; } return null; } catch (err) { diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 83b013353b..6793dda5d4 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -63,7 +63,7 @@ export const useRCAuth = () => { } } } catch (e) { - console.error('A error occurred while setting up user', e); + console.error('An error occurred while setting up user', e); } }; From dbdbebc5a46106340b9d56c18304b58458bb8103 Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 19 Feb 2026 22:46:45 +0530 Subject: [PATCH 25/33] refactor: optimize auth listeners and improve reliability in EmbeddedChat --- packages/react/src/hooks/useRCAuth.js | 86 ++++--- packages/react/src/views/ChatBody/ChatBody.js | 59 +++-- .../react/src/views/Message/MessageToolbox.js | 233 ++++++++++-------- .../common/MessageAggregator.js | 24 +- 4 files changed, 232 insertions(+), 170 deletions(-) diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 83b013353b..d905be40c6 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -25,45 +25,67 @@ export const useRCAuth = () => { const handleLogin = async (userOrEmail, password, code) => { try { const res = await RCInstance.login(userOrEmail, password, code); + + // Handle specific error codes or generic Unauthorized if (res.error === 'Unauthorized' || res.error === 403) { dispatchToastMessage({ type: 'error', - message: - 'Invalid username or password. Please check your credentials and try again', + message: 'Invalid username or password. Please check your credentials.', + }); + return { status: 'error', error: 'Unauthorized' }; + } + + // Handle Two-Factor Authentication (TOTP) + if (res.error === 'totp-required') { + setPassword(password); + setEmailorUser(userOrEmail); + setIsLoginModalOpen(false); + setIsTotpModalOpen(true); + dispatchToastMessage({ + type: 'info', + message: 'MFA Required: Please enter the code from your authenticator app.', }); - } else { - if (res.error === 'totp-required') { - setPassword(password); - setEmailorUser(userOrEmail); - setIsLoginModalOpen(false); - setIsTotpModalOpen(true); - dispatchToastMessage({ - type: 'info', - message: 'Please Open your authentication app and enter the code.', - }); - } else if (res.error === 'totp-invalid') { - dispatchToastMessage({ - type: 'error', - message: 'Invalid TOTP Time-based One-time Password.', - }); - } + return { status: 'totp-required' }; + } + + if (res.error === 'totp-invalid') { + dispatchToastMessage({ + type: 'error', + message: 'Invalid TOTP code. Please try again.', + }); + return { status: 'error', error: 'totp-invalid' }; + } + + // Handle Successful Login + if (res.status === 'success' || (res.me && !res.error)) { + setIsLoginModalOpen(false); + setUserAvatarUrl(res.me.avatarUrl); + setAuthenticatedUserUsername(res.me.username); + setIsUserAuthenticated(true); + setIsTotpModalOpen(false); + + // Clear sensitive temporary data + setEmailorUser(null); + setPassword(null); + + dispatchToastMessage({ + type: 'success', + message: `Welcome back, ${res.me.username}!`, + }); + return { status: 'success', user: res.me }; + } - if (res.status === 'success') { - setIsLoginModalOpen(false); - setUserAvatarUrl(res.me.avatarUrl); - setAuthenticatedUserUsername(res.me.username); - setIsUserAuthenticated(true); - setIsTotpModalOpen(false); - setEmailorUser(null); - setPassword(null); - dispatchToastMessage({ - type: 'success', - message: 'Successfully logged in', - }); - } + // Catch-all for other response errors + if (res.error) { + throw new Error(res.reason || res.error); } } catch (e) { - console.error('A error occurred while setting up user', e); + console.error('Login implementation error:', e); + dispatchToastMessage({ + type: 'error', + message: 'Authentication failed due to a network or server error.', + }); + return { status: 'error', error: e.message }; } }; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 34f5c8bf40..783cc14cfc 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -146,43 +146,46 @@ const ChatBody = ({ ); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { - if (user) { - RCInstance.addMessageListener(addMessage); - RCInstance.addMessageDeleteListener(removeMessage); - RCInstance.addActionTriggeredListener(onActionTriggerResponse); - RCInstance.addUiInteractionListener(onActionTriggerResponse); - } - }); - - return () => { + const removeAllListeners = () => { RCInstance.removeMessageListener(addMessage); RCInstance.removeMessageDeleteListener(removeMessage); RCInstance.removeActionTriggeredListener(onActionTriggerResponse); RCInstance.removeUiInteractionListener(onActionTriggerResponse); }; - }, [RCInstance, addMessage, removeMessage, onActionTriggerResponse]); - useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { + // Clear old listeners before adding new ones to avoid duplicates + removeAllListeners(); + RCInstance.addMessageListener(addMessage); + RCInstance.addMessageDeleteListener(removeMessage); + RCInstance.addActionTriggeredListener(onActionTriggerResponse); + RCInstance.addUiInteractionListener(onActionTriggerResponse); + getMessagesAndRoles(); setHasMoreMessages(true); - } else { - getMessagesAndRoles(anonymousMode); - } - }); - }, [RCInstance, anonymousMode, getMessagesAndRoles]); - - useEffect(() => { - RCInstance.auth.onAuthChange((user) => { - if (user) { fetchAndSetPermissions(); } else { + removeAllListeners(); + getMessagesAndRoles(anonymousMode); permissionsRef.current = null; } }); - }, []); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + removeAllListeners(); + }; + }, [ + RCInstance, + addMessage, + removeMessage, + onActionTriggerResponse, + anonymousMode, + getMessagesAndRoles, + fetchAndSetPermissions, + permissionsRef, + ]); // Expose clearUnreadDivider function via ref for ChatInput to call useEffect(() => { @@ -309,9 +312,15 @@ const ChatBody = ({ useEffect(() => { if (messageListRef.current) { - messageListRef.current.scrollTop = messageListRef.current.scrollHeight; + const { scrollTop, scrollHeight, clientHeight } = messageListRef.current; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 100; + const isInitialLoad = messages.length > 0 && scrollTop === 0; + + if (isAtBottom || isInitialLoad) { + messageListRef.current.scrollTop = scrollHeight; + } } - }, [messages]); + }, [messages, messageListRef]); useEffect(() => { checkOverflow(); diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index 75bdc7467d..d24181144f 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -1,4 +1,4 @@ -import React, { useState, useContext, useMemo } from 'react'; +import React, { useState, useContext, useMemo } from "react"; import { Box, Modal, @@ -8,16 +8,16 @@ import { useComponentOverrides, appendClassNames, useTheme, -} from '@embeddedchat/ui-elements'; -import RCContext from '../../context/RCInstance'; -import { EmojiPicker } from '../EmojiPicker'; -import { getMessageToolboxStyles } from './Message.styles'; -import SurfaceMenu from '../SurfaceMenu/SurfaceMenu'; -import { Markdown } from '../Markdown'; -import Attachment from '../AttachmentHandler/Attachment'; +} from "@embeddedchat/ui-elements"; +import RCContext from "../../context/RCInstance"; +import { EmojiPicker } from "../EmojiPicker"; +import { getMessageToolboxStyles } from "./Message.styles"; +import SurfaceMenu from "../SurfaceMenu/SurfaceMenu"; +import { Markdown } from "../Markdown"; +import Attachment from "../AttachmentHandler/Attachment"; export const MessageToolbox = ({ - className = '', + className = "", message, variantStyles = {}, style = {}, @@ -42,16 +42,16 @@ export const MessageToolbox = ({ isEditing = false, optionConfig = { surfaceItems: [ - 'reaction', - 'reply', - 'quote', - 'star', - 'copy', - 'link', - 'pin', - 'edit', - 'delete', - 'report', + "reaction", + "reply", + "quote", + "star", + "copy", + "link", + "pin", + "edit", + "delete", + "report", ], menuItems: [], @@ -60,9 +60,9 @@ export const MessageToolbox = ({ ...props }) => { const { styleOverrides, classNames, configOverrides } = useComponentOverrides( - 'MessageToolbox', + "MessageToolbox", className, - style + style, ); const { RCInstance } = useContext(RCContext); const instanceHost = RCInstance.getHost(); @@ -81,121 +81,148 @@ export const MessageToolbox = ({ setShowDeleteModal(false); }; - const isAllowedToPin = userRoles.some((role) => pinRoles.has(role)); + const { + isAllowedToPin, + isAllowedToReport, + isAllowedToEditMessage, + isAllowedToDeleteMessage, + isAllowedToDeleteOwnMessage, + isAllowedToForceDeleteMessage, + isVisibleForMessageType, + canDeleteMessage, + } = useMemo(() => { + const isOwner = message.u._id === authenticatedUserId; + const allowedToPin = userRoles.some((role) => pinRoles.has(role)); + const allowedToReport = !isOwner; + const allowedToEdit = + userRoles.some((role) => editMessageRoles.has(role)) || isOwner; + const allowedToDelete = userRoles.some((role) => + deleteMessageRoles.has(role), + ); + const allowedToDeleteOwn = userRoles.some((role) => + deleteOwnMessageRoles.has(role), + ); + const allowedToForceDelete = userRoles.some((role) => + forceDeleteMessageRoles.has(role), + ); - const isAllowedToReport = message.u._id !== authenticatedUserId; + const visibleForMessageType = + message.files?.[0]?.type !== "audio/mpeg" && + message.files?.[0]?.type !== "video/mp4"; - const isAllowedToEditMessage = userRoles.some((role) => - editMessageRoles.has(role) - ) - ? true - : message.u._id === authenticatedUserId; + const canDelete = allowedToForceDelete + ? true + : allowedToDelete + ? true + : allowedToDeleteOwn + ? isOwner + : false; - const isAllowedToDeleteMessage = userRoles.some((role) => - deleteMessageRoles.has(role) - ); - const isAllowedToDeleteOwnMessage = userRoles.some((role) => - deleteOwnMessageRoles.has(role) - ); - const isAllowedToForceDeleteMessage = userRoles.some((role) => - forceDeleteMessageRoles.has(role) - ); - - const isVisibleForMessageType = - message.files?.[0].type !== 'audio/mpeg' && - message.files?.[0].type !== 'video/mp4'; - - const canDeleteMessage = isAllowedToForceDeleteMessage - ? true - : isAllowedToDeleteMessage - ? true - : isAllowedToDeleteOwnMessage - ? message.u._id === authenticatedUserId - : false; + return { + isAllowedToPin: allowedToPin, + isAllowedToReport: allowedToReport, + isAllowedToEditMessage: allowedToEdit, + isAllowedToDeleteMessage: allowedToDelete, + isAllowedToDeleteOwnMessage: allowedToDeleteOwn, + isAllowedToForceDeleteMessage: allowedToForceDelete, + isVisibleForMessageType: visibleForMessageType, + canDeleteMessage: canDelete, + }; + }, [ + authenticatedUserId, + userRoles, + pinRoles, + deleteMessageRoles, + deleteOwnMessageRoles, + forceDeleteMessageRoles, + editMessageRoles, + message.u._id, + message.files, + ]); const options = useMemo( () => ({ reply: { - label: 'Reply in thread', - id: 'reply', + label: "Reply in thread", + id: "reply", onClick: handleOpenThread(message), - iconName: 'thread', + iconName: "thread", visible: !isThreadMessage, }, quote: { - label: 'Quote', - id: 'quote', + label: "Quote", + id: "quote", onClick: () => handleQuoteMessage(message), - iconName: 'quote', + iconName: "quote", visible: true, }, star: { label: message.starred && message.starred.find((u) => u._id === authenticatedUserId) - ? 'Unstar' - : 'Star', - id: 'star', + ? "Unstar" + : "Star", + id: "star", onClick: () => handleStarMessage(message), iconName: message.starred && message.starred.find((u) => u._id === authenticatedUserId) - ? 'star-filled' - : 'star', + ? "star-filled" + : "star", visible: true, }, reaction: { - label: 'Add reaction', - id: 'reaction', + label: "Add reaction", + id: "reaction", onClick: () => setEmojiOpen(true), - iconName: 'emoji', + iconName: "emoji", visible: true, }, pin: { - label: message.pinned ? 'Unpin' : 'Pin', - id: 'pin', + label: message.pinned ? "Unpin" : "Pin", + id: "pin", onClick: () => handlePinMessage(message), - iconName: message.pinned ? 'pin-filled' : 'pin', + iconName: message.pinned ? "pin-filled" : "pin", visible: isAllowedToPin, }, edit: { - label: 'Edit', - id: 'edit', + label: "Edit", + id: "edit", onClick: () => handleEditMessage(message), - iconName: 'edit', + iconName: "edit", visible: isAllowedToEditMessage, - color: isEditing ? 'secondary' : 'default', + color: isEditing ? "secondary" : "default", ghost: !isEditing, }, copy: { - label: 'Copy message', - id: 'copy', + label: "Copy message", + id: "copy", onClick: () => handleCopyMessage(message), - iconName: 'copy', + iconName: "copy", visible: true, }, link: { - label: 'Copy link', - id: 'link', + label: "Copy link", + id: "link", onClick: () => handleCopyMessageLink(message), - iconName: 'link', + iconName: "link", visible: true, }, delete: { - label: 'Delete', - id: 'delete', + label: "Delete", + id: "delete", onClick: () => setShowDeleteModal(true), - iconName: 'trash', + iconName: "trash", visible: canDeleteMessage, - type: 'destructive', + type: "destructive", }, report: { - label: 'Report', - id: 'report', + label: "Report", + id: "report", onClick: () => handlerReportMessage(message), - iconName: 'report', + iconName: "report", visible: isAllowedToReport, - type: 'destructive', + type: "destructive", }, }), [ @@ -210,8 +237,12 @@ export const MessageToolbox = ({ handleEditMessage, handlerReportMessage, handleCopyMessage, + handleCopyMessageLink, isAllowedToPin, - ] + isAllowedToReport, + isAllowedToEditMessage, + canDeleteMessage, + ], ); const menuOptions = menuItems @@ -248,7 +279,7 @@ export const MessageToolbox = ({ )} ); }; + +MessageAggregator.propTypes = { + title: PropTypes.string.isRequired, + iconName: PropTypes.string, + noMessageInfo: PropTypes.string, + shouldRender: PropTypes.func.isRequired, + fetchedMessageList: PropTypes.array, + filterProps: PropTypes.object, + searchProps: PropTypes.object, + searchFiltered: PropTypes.array, + fetching: PropTypes.bool, + type: PropTypes.oneOf(['message', 'file']), + viewType: PropTypes.oneOf(['Sidebar', 'Popup']), +}; + +export default MessageAggregator; diff --git a/packages/react/src/views/TypingUsers/TypingUsers.js b/packages/react/src/views/TypingUsers/TypingUsers.js index db05619ec1..9982e6f6ab 100644 --- a/packages/react/src/views/TypingUsers/TypingUsers.js +++ b/packages/react/src/views/TypingUsers/TypingUsers.js @@ -11,10 +11,11 @@ export default function TypingUsers() { const { theme } = useTheme(); useEffect(() => { - RCInstance.addTypingStatusListener((t) => { + const handleTypingStatus = (t) => { setTypingUsers((t || []).filter((u) => u !== currentUserName)); - }); - return () => RCInstance.removeTypingStatusListener(setTypingUsers); + }; + RCInstance.addTypingStatusListener(handleTypingStatus); + return () => RCInstance.removeTypingStatusListener(handleTypingStatus); }, [RCInstance, setTypingUsers, currentUserName]); const typingStatusMessage = useMemo(() => { From b19b5fa441fc78708201ed07f4316c305f8909f2 Mon Sep 17 00:00:00 2001 From: vivek @@ -259,9 +290,9 @@ export const MessageToolbox = ({ )} @@ -288,36 +319,36 @@ export const MessageToolbox = ({ {' '} + style={{ marginRight: "0.5rem" }} + />{" "} Delete this message? {message.file ? ( - message.file.type.startsWith('image/') ? ( + message.file.type.startsWith("image/") ? ( - ) : message.file.type.startsWith('video/') ? ( + ) : message.file.type.startsWith("video/") ? ( - ) : message.file.type.startsWith('audio/') ? ( -
{`${message.file.name} (${( message.file.size / 1024 ).toFixed(2)} kB)`}{avatarUrl && ( - @@ -458,4 +458,10 @@ export default ChatBody; ChatBody.propTypes = { anonymousMode: PropTypes.bool, showRoles: PropTypes.bool, + messageListRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + scrollToBottom: PropTypes.func, + clearUnreadDividerRef: PropTypes.shape({ current: PropTypes.func }), }; diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index f3b94c7b48..b9f88e15e2 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -18,7 +18,7 @@ import { import { ChatLayout } from './ChatLayout'; import { ChatHeader } from './ChatHeader'; import { RCInstanceProvider } from '../context/RCInstance'; -import { useUserStore, useLoginStore, useMessageStore } from '../store'; +import { useUserStore, useLoginStore } from '../store'; import DefaultTheme from '../theme/DefaultTheme'; import { getTokenStorage } from '../lib/auth'; import { styles } from './EmbeddedChat.styles'; @@ -134,7 +134,7 @@ const EmbeddedChat = (props) => { }, [RCInstance, auth, setIsLoginIn]); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.connect() .then(() => { @@ -152,6 +152,10 @@ const EmbeddedChat = (props) => { setIsUserAuthenticated(false); } }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; }, [ RCInstance, setAuthenticatedName, diff --git a/packages/react/src/views/LoginForm/LoginForm.js b/packages/react/src/views/LoginForm/LoginForm.js index 1285e5e1eb..9c3f945c92 100644 --- a/packages/react/src/views/LoginForm/LoginForm.js +++ b/packages/react/src/views/LoginForm/LoginForm.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; import { css } from '@emotion/react'; import { GenericModal, @@ -12,7 +13,7 @@ import { useLoginStore } from '../../store'; import { useRCAuth } from '../../hooks/useRCAuth'; import styles from './LoginForm.styles'; -export default function LoginForm() { +const LoginForm = () => { const [userOrEmail, setUserOrEmail] = useState(null); const [password, setPassword] = useState(null); const [showPassword, setShowPassword] = useState(false); @@ -108,6 +109,7 @@ export default function LoginForm() { /> {field.label === 'Password' && (+
)} {surfaceOptions.length > 0 && ( diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index e753b689ae..c87de628b0 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -144,7 +144,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { ); useEffect(() => { - RCInstance.auth.onAuthChange((user) => { + const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { RCInstance.getCommandsList() .then((data) => setCommands(data.commands || [])) @@ -157,6 +157,11 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { .catch(console.error); } }); + + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + if (timerRef.current) clearTimeout(timerRef.current); + }; }, [RCInstance, isChannelPrivate, setMembersHandler]); useEffect(() => { @@ -258,13 +263,14 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { return; } if (messageRef.current.value?.length) { + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = true; timerRef.current = setTimeout(() => { typingRef.current = false; - }, [15000]); + }, 15000); await RCInstance.sendTypingStatus(username, true); } else { - clearTimeout(timerRef.current); + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = false; await RCInstance.sendTypingStatus(username, false); } @@ -275,6 +281,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const sendTypingStop = async () => { try { + if (timerRef.current) clearTimeout(timerRef.current); typingRef.current = false; await RCInstance.sendTypingStatus(username, false); } catch (e) { From feb14f95b03786f6f7206485d1a7a0d0bc7a56df Mon Sep 17 00:00:00 2001 From: vivek Date: Thu, 19 Feb 2026 23:46:22 +0530 Subject: [PATCH 27/33] chore: major quality overhaul - fix unstable keys, add propTypes, and resolve listener leaks --- .../src/views/ChannelState/ChannelState.js | 9 ++ packages/react/src/views/ChatBody/ChatBody.js | 8 +- packages/react/src/views/EmbeddedChat.js | 8 +- .../react/src/views/LoginForm/LoginForm.js | 10 +- .../common/MessageAggregator.js | 130 ++++++++++-------- .../src/views/TypingUsers/TypingUsers.js | 7 +- 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/packages/react/src/views/ChannelState/ChannelState.js b/packages/react/src/views/ChannelState/ChannelState.js index f5262d3edb..90ef8c5347 100644 --- a/packages/react/src/views/ChannelState/ChannelState.js +++ b/packages/react/src/views/ChannelState/ChannelState.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Box, Icon, @@ -34,4 +35,12 @@ const ChannelState = ({ ); }; +ChannelState.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + status: PropTypes.string, + iconName: PropTypes.string, + instructions: PropTypes.string, +}; + export default ChannelState; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 783cc14cfc..b0f5dda15e 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -438,7 +438,7 @@ const ChatBody = ({ {uiKitModalOpen && ( - + )} > ) : null; -} +}; + +LoginForm.propTypes = {}; // No props, but good to have for consistency + +export default LoginForm; diff --git a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js index 9c4c652561..7cce39cb06 100644 --- a/packages/react/src/views/MessageAggregators/common/MessageAggregator.js +++ b/packages/react/src/views/MessageAggregators/common/MessageAggregator.js @@ -1,4 +1,5 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; +import PropTypes from 'prop-types'; import { isSameDay, format } from 'date-fns'; import { Box, @@ -21,7 +22,7 @@ import FileDisplay from '../../FileMessage/FileMessage'; import useSetExclusiveState from '../../../hooks/useSetExclusiveState'; import { useRCContext } from '../../../context/RCInstance'; -export const MessageAggregator = ({ +const MessageAggregator = ({ title, iconName, noMessageInfo, @@ -128,9 +129,9 @@ export const MessageAggregator = ({ }; useEffect(() => { - const hasRendered = uniqueMessageList.some((msg) => shouldRender(msg)); + const hasRendered = messageList.some((msg) => shouldRender(msg)); setMessageRendered(hasRendered); - }, [uniqueMessageList, shouldRender]); + }, [messageList, shouldRender]); const isMessageNewDay = (current, previous) => { if (!previous || shouldRender(previous)) return true; @@ -171,65 +172,80 @@ export const MessageAggregator = ({ + )} + + ); + })})} - {uniqueMessageList.map((msg, index, arr) => { - const newDay = isMessageNewDay(msg, arr[index - 1]); - - return ( - - {type === 'message' && newDay && ( - - {format(new Date(msg.ts), 'MMMM d, yyyy')} - - )} - {type === 'file' ? ( -{ + const newDay = isMessageNewDay(msg, arr[index - 1]); + + return ( + + {type === 'message' && newDay && ( + - ); - } - )} ++ {format(new Date(msg.ts), 'MMMM d, yyyy')} + + )} + {type === 'file' ? ( ++ ) : ( + + - )} -- ) : ( - + + setJumpToMessage(msg)} + css={{ + position: 'relative', + zIndex: 10, + marginRight: '5px', }} > - - - setJumpToMessage(msg)} - css={{ - position: 'relative', - zIndex: 10, - marginRight: '5px', - }} - > - -- + + Date: Fri, 20 Feb 2026 09:36:35 +0530 Subject: [PATCH 28/33] fix: critical UI crashes, session leaks, and emoji picker defaults - Prevented crash on unknown slash commands in CommandsList\n- Improved command list visibility logic in useShowCommands\n- Fixed session state leaks by resetting user store on logout in EmbeddedChat\n- Removed hardcoded default emoji preview in EmojiPicker --- packages/react/src/hooks/useShowCommands.js | 5 +++-- packages/react/src/views/CommandList/CommandsList.js | 1 + packages/react/src/views/EmbeddedChat.js | 5 +++++ packages/react/src/views/EmojiPicker/EmojiPicker.js | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react/src/hooks/useShowCommands.js b/packages/react/src/hooks/useShowCommands.js index 1379f267c4..4e10c98052 100644 --- a/packages/react/src/hooks/useShowCommands.js +++ b/packages/react/src/hooks/useShowCommands.js @@ -10,8 +10,9 @@ const useShowCommands = (commands, setFilteredCommands, setShowCommandList) => const tokens = e.target.value.slice(0, cursor).split(/\s+/); if (tokens.length === 1 && tokens[0].startsWith('/')) { - setFilteredCommands(getFilteredCommands(tokens[0])); - setShowCommandList(true); + const filtered = getFilteredCommands(tokens[0]); + setFilteredCommands(filtered); + setShowCommandList(filtered.length > 0); } else { setFilteredCommands([]); setShowCommandList(false); diff --git a/packages/react/src/views/CommandList/CommandsList.js b/packages/react/src/views/CommandList/CommandsList.js index ad4d9f1d45..db97fe45f2 100644 --- a/packages/react/src/views/CommandList/CommandsList.js +++ b/packages/react/src/views/CommandList/CommandsList.js @@ -30,6 +30,7 @@ function CommandsList({ const handleCommandClick = useCallback( async (command) => { + if (!command) return; const commandName = command.command; const currentMessage = messageRef.current.value; const tokens = (currentMessage || '').split(' '); diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index b9f88e15e2..73b434d22a 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -150,6 +150,11 @@ const EmbeddedChat = (props) => { .catch(console.error); } else { setIsUserAuthenticated(false); + setAuthenticatedAvatarUrl(''); + setAuthenticatedUsername(''); + setAuthenticatedUserId(''); + setAuthenticatedName(''); + setAuthenticatedUserRoles([]); } }); diff --git a/packages/react/src/views/EmojiPicker/EmojiPicker.js b/packages/react/src/views/EmojiPicker/EmojiPicker.js index 6501eb8320..92557b4b8b 100644 --- a/packages/react/src/views/EmojiPicker/EmojiPicker.js +++ b/packages/react/src/views/EmojiPicker/EmojiPicker.js @@ -18,7 +18,6 @@ const CustomEmojiPicker = ({ const theme = useTheme(); const styles = getEmojiPickerStyles(theme); const previewConfig = { - defaultEmoji: '1f60d', defaultCaption: 'None', showPreview: true, }; From 656adfb6b9286d3c79f9b3b3a841bac4a676da0b Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 20 Feb 2026 13:08:45 +0530 Subject: [PATCH 29/33] fix: prevent crash when typing unknown slash commands --- packages/react/src/views/CommandList/CommandsList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/views/CommandList/CommandsList.js b/packages/react/src/views/CommandList/CommandsList.js index db97fe45f2..7afa4264a7 100644 --- a/packages/react/src/views/CommandList/CommandsList.js +++ b/packages/react/src/views/CommandList/CommandsList.js @@ -30,6 +30,7 @@ function CommandsList({ const handleCommandClick = useCallback( async (command) => { + if (!command) return; if (!command) return; const commandName = command.command; const currentMessage = messageRef.current.value; From 33d639877f19b8e59c637065af00bd8d1ce1dacf Mon Sep 17 00:00:00 2001 From: vivek Date: Mon, 23 Feb 2026 18:59:21 +0530 Subject: [PATCH 30/33] fix: safety checks for command list to prevent crashes --- packages/react/src/views/CommandList/CommandsList.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react/src/views/CommandList/CommandsList.js b/packages/react/src/views/CommandList/CommandsList.js index 7afa4264a7..29a5bdf505 100644 --- a/packages/react/src/views/CommandList/CommandsList.js +++ b/packages/react/src/views/CommandList/CommandsList.js @@ -30,8 +30,7 @@ function CommandsList({ const handleCommandClick = useCallback( async (command) => { - if (!command) return; - if (!command) return; + if (!command || !messageRef.current) return; const commandName = command.command; const currentMessage = messageRef.current.value; const tokens = (currentMessage || '').split(' '); @@ -100,7 +99,7 @@ function CommandsList({ {filteredCommands.map((command, index) => (
- setItemRef(el, index)} From abd797e60df4e47c37bf7eb2c22cac6c3fb5fec3 Mon Sep 17 00:00:00 2001 From: vivek
Date: Tue, 24 Feb 2026 20:40:02 +0530 Subject: [PATCH 31/33] feat: implement AI Adapter layer, State Machines for Auth/Input, and Contextual AI Features (Smart Replies, Summary, Translation) --- packages/api/src/EmbeddedChatApi.ts | 34 ++++ packages/api/src/IAiAdapter.ts | 1 + packages/api/src/MockAiAdapter.ts | 5 + packages/api/src/index.ts | 2 + packages/auth/src/RocketChatAuth.ts | 157 +++++++++++---- packages/auth/src/index.ts | 2 +- packages/react/src/hooks/useMediaRecorder.js | 10 +- packages/react/src/hooks/useRCAuth.js | 12 +- packages/react/src/store/aiStore.js | 12 ++ packages/react/src/store/index.js | 1 + packages/react/src/store/userStore.js | 2 + packages/react/src/views/ChatBody/ChatBody.js | 34 +++- .../react/src/views/ChatHeader/ChatHeader.js | 19 ++ .../react/src/views/ChatInput/ChatInput.js | 119 ++++++++--- .../ChatInput/ChatInputFormattingToolbar.js | 9 +- .../react/src/views/ChatInput/SmartReplies.js | 61 ++++++ .../src/views/DynamicHeader/DynamicHeader.js | 2 + packages/react/src/views/EmbeddedChat.js | 5 +- packages/react/src/views/Message/Message.js | 82 ++++++-- .../react/src/views/Message/MessageToolbox.js | 10 + temp_lerna_index.js | 185 ++++++++++++++++++ test_simple.js | 1 + 22 files changed, 666 insertions(+), 99 deletions(-) create mode 100644 packages/react/src/store/aiStore.js create mode 100644 packages/react/src/views/ChatInput/SmartReplies.js create mode 100644 temp_lerna_index.js create mode 100644 test_simple.js diff --git a/packages/api/src/EmbeddedChatApi.ts b/packages/api/src/EmbeddedChatApi.ts index 4e00d18397..399aa64db2 100644 --- a/packages/api/src/EmbeddedChatApi.ts +++ b/packages/api/src/EmbeddedChatApi.ts @@ -72,6 +72,40 @@ export default class EmbeddedChatApi { return this.aiAdapter; } + async getSmartReplies() { + if (this.aiAdapter && this.aiAdapter.enabled) { + const { messages } = await this.getMessages(false, { + query: { tmid: { $exists: false } }, + }); + return this.aiAdapter.getSmartReplies(messages.slice(0, 10)); // Context of last 10 messages + } + return []; + } + + async getSummary() { + if (this.aiAdapter && this.aiAdapter.enabled) { + const { messages } = await this.getMessages(false, { + query: { tmid: { $exists: false } }, + }); + return this.aiAdapter.getSummary(messages.slice(0, 50)); // Last 50 messages for summary + } + return "AI Adapter is not enabled or available."; + } + + async translateMessage(text: string, targetLanguage: string) { + if (this.aiAdapter && this.aiAdapter.enabled) { + return this.aiAdapter.translateMessage(text, targetLanguage); + } + return text; + } + + async handleAiCommand(command: string, params: any) { + if (this.aiAdapter && this.aiAdapter.enabled) { + return this.aiAdapter.onCommand(command, params); + } + return null; + } + /** * Todo refactor */ diff --git a/packages/api/src/IAiAdapter.ts b/packages/api/src/IAiAdapter.ts index 1b4bc8ae21..c6dc3c89ef 100644 --- a/packages/api/src/IAiAdapter.ts +++ b/packages/api/src/IAiAdapter.ts @@ -4,5 +4,6 @@ export interface IAiAdapter { enabled: boolean; getSmartReplies: (messageContext: any[]) => Promise ; getSummary: (messages: any[]) => Promise ; + translateMessage: (text: string, targetLanguage: string) => Promise ; onCommand: (command: string, params: any) => Promise ; } diff --git a/packages/api/src/MockAiAdapter.ts b/packages/api/src/MockAiAdapter.ts index 279cb2a672..82c41ec465 100644 --- a/packages/api/src/MockAiAdapter.ts +++ b/packages/api/src/MockAiAdapter.ts @@ -15,6 +15,11 @@ export class MockAiAdapter implements IAiAdapter { return "This is a mock summary of the conversation."; } + async translateMessage(text: string, targetLanguage: string): Promise { + console.log("Mock AI: Translating text to", targetLanguage); + return `[Translated to ${targetLanguage}]: ${text}`; + } + async onCommand(command: string, params: any): Promise { console.log("Mock AI: Handling command", command, "with params", params); if (command === "help") { diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b4ff83a1c6..5921b279b2 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1 +1,3 @@ export { default as EmbeddedChatApi } from "./EmbeddedChatApi"; +export { MockAiAdapter } from "./MockAiAdapter"; +export { IAiAdapter } from "./IAiAdapter"; diff --git a/packages/auth/src/RocketChatAuth.ts b/packages/auth/src/RocketChatAuth.ts index 0f2c55f196..4c3a7fbfd2 100644 --- a/packages/auth/src/RocketChatAuth.ts +++ b/packages/auth/src/RocketChatAuth.ts @@ -5,15 +5,35 @@ import { IRocketChatAuthOptions } from "./IRocketChatAuthOptions"; import { Api, ApiError } from "./Api"; import loginWithRocketChatOAuth from "./loginWithRocketChatOAuth"; import handleSecureLogin from "./handleSecureLogin"; +export enum AuthState { + IDLE = "IDLE", + AUTHENTICATING = "AUTHENTICATING", + AUTHENTICATED = "AUTHENTICATED", + UNAUTHENTICATED = "UNAUTHENTICATED", + RECONNECTING = "RECONNECTING", + ERROR = "ERROR", +} + class RocketChatAuth { host: string; api: Api; currentUser: any; lastFetched: Date; authListeners: ((user: object | null) => void)[] = []; + stateListeners: ((state: AuthState) => void)[] = []; + private _state: AuthState = AuthState.IDLE; deleteToken: () => Promise ; saveToken: (token: string) => Promise ; getToken: () => Promise ; + + get state() { + return this._state; + } + + set state(newState: AuthState) { + this._state = newState; + this.notifyStateListeners(); + } constructor({ host, saveToken, @@ -29,10 +49,6 @@ class RocketChatAuth { this.deleteToken = deleteToken; } - /** - * Add a callback that will be called when user login status changes - * @param callback - */ async onAuthChange(callback: (user: object | null) => void) { this.authListeners.push(callback); const user = await this.getCurrentUser(); @@ -47,6 +63,19 @@ class RocketChatAuth { this.authListeners.forEach((cb) => cb(this.currentUser)); } + onStateChange(callback: (state: AuthState) => void) { + this.stateListeners.push(callback); + callback(this.state); + } + + removeStateListener(callback: (state: AuthState) => void) { + this.stateListeners = this.stateListeners.filter((cb) => cb !== callback); + } + + notifyStateListeners() { + this.stateListeners.forEach((cb) => cb(this.state)); + } + /** * Login with username and password * @param credentials @@ -61,18 +90,31 @@ class RocketChatAuth { password: string; code?: string | number; }) { - const response = await loginWithPassword( - { - api: this.api, - }, - { - user, - password, - code, + this.state = AuthState.AUTHENTICATING; + try { + const response = await loginWithPassword( + { + api: this.api, + }, + { + user, + password, + code, + } + ); + this.setUser(response.data); + this.state = AuthState.AUTHENTICATED; + return this.currentUser; + } catch (error) { + if ( + !(error instanceof ApiError && (error as any).response?.status === 401) + ) { + this.state = AuthState.ERROR; + } else { + this.state = AuthState.UNAUTHENTICATED; } - ); - this.setUser(response.data); - return this.currentUser; + throw error; + } } /** @@ -85,14 +127,21 @@ class RocketChatAuth { service: string; access_token: string; }) { - const response = await loginWithOAuthServiceToken( - { - api: this.api, - }, - credentials - ); - this.setUser(response.data); - return this.currentUser; + this.state = AuthState.AUTHENTICATING; + try { + const response = await loginWithOAuthServiceToken( + { + api: this.api, + }, + credentials + ); + this.setUser(response.data); + this.state = AuthState.AUTHENTICATED; + return this.currentUser; + } catch (error) { + this.state = AuthState.ERROR; + throw error; + } } /** @@ -100,14 +149,23 @@ class RocketChatAuth { * @returns */ async loginWithRocketChatOAuth() { - if (typeof window === "undefined") { - throw new Error("loginWithRocketChatOAuth can only be called in browser"); + this.state = AuthState.AUTHENTICATING; + try { + if (typeof window === "undefined") { + throw new Error( + "loginWithRocketChatOAuth can only be called in browser" + ); + } + const response = await loginWithRocketChatOAuth({ + api: this.api, + }); + this.setUser(response.data); + this.state = AuthState.AUTHENTICATED; + return this.currentUser; + } catch (error) { + this.state = AuthState.ERROR; + throw error; } - const response = await loginWithRocketChatOAuth({ - api: this.api, - }); - this.setUser(response.data); - return this.currentUser; } /** @@ -116,16 +174,23 @@ class RocketChatAuth { * @returns */ async loginWithResumeToken(resume: string) { - const response = await loginWithResumeToken( - { - api: this.api, - }, - { - resume, - } - ); - this.setUser(response.data); - return this.currentUser; + this.state = AuthState.RECONNECTING; + try { + const response = await loginWithResumeToken( + { + api: this.api, + }, + { + resume, + } + ); + this.setUser(response.data); + this.state = AuthState.AUTHENTICATED; + return this.currentUser; + } catch (error) { + this.state = AuthState.UNAUTHENTICATED; + throw error; + } } /** @@ -190,14 +255,21 @@ class RocketChatAuth { try { const token = await this.getToken(); if (token) { - const user = await this.loginWithResumeToken(token); // will notifyAuthListeners on successful login + this.state = AuthState.RECONNECTING; + const user = await this.loginWithResumeToken(token); if (user) { this.lastFetched = new Date(); - await this.getCurrentUser(); // refresh the token if needed + await this.getCurrentUser(); + this.state = AuthState.AUTHENTICATED; + } else { + this.state = AuthState.UNAUTHENTICATED; } + } else { + this.state = AuthState.UNAUTHENTICATED; } } catch (e) { console.log("Failed to login user on initial load. Sign in."); + this.state = AuthState.UNAUTHENTICATED; this.notifyAuthListeners(); } } @@ -220,6 +292,7 @@ class RocketChatAuth { } this.lastFetched = new Date(0); this.currentUser = null; + this.state = AuthState.UNAUTHENTICATED; this.notifyAuthListeners(); } } diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index ff36dfb6ab..56dd8dcebb 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,4 +1,4 @@ export * from "./auth"; -export { default as RocketChatAuth } from "./RocketChatAuth"; +export { default as RocketChatAuth, AuthState } from "./RocketChatAuth"; export * from "./IRocketChatAuthOptions"; export { ApiError } from "./Api"; diff --git a/packages/react/src/hooks/useMediaRecorder.js b/packages/react/src/hooks/useMediaRecorder.js index 028b0d9b1d..aed5cd71ad 100644 --- a/packages/react/src/hooks/useMediaRecorder.js +++ b/packages/react/src/hooks/useMediaRecorder.js @@ -40,11 +40,19 @@ export function useMediaRecorder({ constraints, onStop, videoRef }) { async function stop() { if (recorder) { - recorder.stop(); + if (recorder.state === 'recording') { + recorder.stop(); + } (await getStream()).getTracks().forEach((track) => track.stop()); } } + useEffect(() => () => { + if (recorder && recorder.state === 'recording') { + recorder.stop(); + } + }, [recorder]); + return [start, stop]; } diff --git a/packages/react/src/hooks/useRCAuth.js b/packages/react/src/hooks/useRCAuth.js index 2e9ecd86b4..10fe3ed211 100644 --- a/packages/react/src/hooks/useRCAuth.js +++ b/packages/react/src/hooks/useRCAuth.js @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; import { useToastBarDispatch } from '@embeddedchat/ui-elements'; import RCContext from '../context/RCInstance'; import { useUserStore, totpModalStore, useLoginStore } from '../store'; @@ -20,8 +20,18 @@ export const useRCAuth = () => { ); const setPassword = useUserStore((state) => state.setPassword); const setEmailorUser = useUserStore((state) => state.setEmailorUser); + const setAuthState = useUserStore((state) => state.setAuthState); const dispatchToastMessage = useToastBarDispatch(); + useEffect(() => { + const handleStateChange = (state) => { + setAuthState(state); + }; + + RCInstance.auth.onStateChange(handleStateChange); + return () => RCInstance.auth.removeStateListener(handleStateChange); + }, [RCInstance, setAuthState]); + const handleLogin = async (userOrEmail, password, code) => { try { const res = await RCInstance.login(userOrEmail, password, code); diff --git a/packages/react/src/store/aiStore.js b/packages/react/src/store/aiStore.js new file mode 100644 index 0000000000..d618105898 --- /dev/null +++ b/packages/react/src/store/aiStore.js @@ -0,0 +1,12 @@ +import { create } from 'zustand'; + +const useAiStore = create((set) => ({ + smartReplies: [], + setSmartReplies: (replies) => set({ smartReplies: replies }), + isAiEnabled: false, + setIsAiEnabled: (enabled) => set({ isAiEnabled: enabled }), + aiLoading: false, + setAiLoading: (loading) => set({ aiLoading: loading }), +})); + +export default useAiStore; diff --git a/packages/react/src/store/index.js b/packages/react/src/store/index.js index bddd0bd1d6..c5f0c0018a 100644 --- a/packages/react/src/store/index.js +++ b/packages/react/src/store/index.js @@ -11,3 +11,4 @@ export { default as useMentionsStore } from './mentionsStore'; export { default as usePinnedMessageStore } from './pinnedMessageStore'; export { default as useStarredMessageStore } from './starredMessageStore'; export { default as useSidebarStore } from './sidebarStore'; +export { default as useAiStore } from './aiStore'; diff --git a/packages/react/src/store/userStore.js b/packages/react/src/store/userStore.js index a19fef4714..fb4dc1920f 100644 --- a/packages/react/src/store/userStore.js +++ b/packages/react/src/store/userStore.js @@ -40,6 +40,8 @@ const useUserStore = create((set) => ({ set(() => ({ showCurrentUserInfo })), currentUser: {}, setCurrentUser: (currentUser) => set({ currentUser }), + authState: 'IDLE', + setAuthState: (authState) => set({ authState }), })); export default useUserStore; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index b0f5dda15e..531ede2a43 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -82,11 +82,26 @@ const ChatBody = ({ const { handleLogin } = useRCAuth(); const { handleServerInteraction } = useUiKitActionManager(); - const isUserAuthenticated = useUserStore( - (state) => state.isUserAuthenticated + const { username, authState, isUserAuthenticated } = useUserStore( + (state) => ({ + username: state.username, + authState: state.authState, + isUserAuthenticated: state.isUserAuthenticated, + }) ); - const username = useUserStore((state) => state.username); + const getAuthStateMessage = () => { + switch (authState) { + case 'AUTHENTICATING': + return 'Logging in...'; + case 'RECONNECTING': + return 'Refreshing session...'; + case 'IDLE': + case 'AUTHENTICATED': + default: + return 'Connecting...'; + } + }; const { getMessagesAndRoles, fetchAndSetPermissions, permissionsRef } = useFetchChatData(showRoles); @@ -410,7 +425,9 @@ const ChatBody = ({ }} className={`ec-chat-body ${classNames}`} > - {isLoginIn ? ( + {((authState !== 'AUTHENTICATED' && authState !== 'UNAUTHENTICATED') || + !useMessageStore.getState().isMessageLoaded) && + !anonymousMode ? ( ) : isThreadOpen ? ( + + {getAuthStateMessage()} + { + const summary = await RCInstance.getSummary(); + dispatchToastMessage({ + type: 'info', + message: summary, + stay: true, + }); + }} + title="Summarize thread" + > + + + ) + } /> )} diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index d153e299c9..908f64c442 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -27,6 +27,8 @@ import { TypingUsers } from '../TypingUsers'; import createPendingMessage from '../../lib/createPendingMessage'; import { CommandsList } from '../CommandList'; import useSettingsStore from '../../store/settingsStore'; +import useAiStore from '../../store/aiStore'; +import SmartReplies from './SmartReplies'; import ChannelState from '../ChannelState/ChannelState'; import QuoteMessage from '../QuoteMessage/QuoteMessage'; import { getChatInputStyles } from './ChatInput.styles'; @@ -58,6 +60,15 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const [filteredCommands, setFilteredCommands] = useState([]); const [isMsgLong, setIsMsgLong] = useState(false); + const InputState = { + IDLE: 'IDLE', + DRAFTING: 'DRAFTING', + SENDING: 'SENDING', + ERROR: 'ERROR', + }; + + const [inputState, setInputState] = useState(InputState.IDLE); + const { isUserAuthenticated, canSendMsg, @@ -143,6 +154,32 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { setShowMembersList ); + const setSmartReplies = useAiStore((state) => state.setSmartReplies); + + const fetchSmartReplies = useCallback(async () => { + if (RCInstance.getAiAdapter()?.enabled) { + const replies = await RCInstance.getSmartReplies(); + setSmartReplies(replies); + } + }, [RCInstance, setSmartReplies]); + + useEffect(() => { + const handleMessage = (message) => { + // Only fetch if it's not our own message + if (message.u.username !== userInfo.username) { + fetchSmartReplies(); + } + }; + RCInstance.addMessageListener(handleMessage); + return () => RCInstance.removeMessageListener(handleMessage); + }, [RCInstance, userInfo.username, fetchSmartReplies]); + + const handleSmartReplyClick = (reply) => { + setInputState(InputState.DRAFTING); + messageRef.current.value = reply; + messageRef.current.focus(); + }; + useEffect(() => { const unsubscribe = RCInstance.auth.onAuthChange((user) => { if (user) { @@ -267,7 +304,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { typingRef.current = true; timerRef.current = setTimeout(() => { typingRef.current = false; - }, 15000); + }, 10000); await RCInstance.sendTypingStatus(username, true); } else { if (timerRef.current) clearTimeout(timerRef.current); @@ -290,6 +327,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { }; const handleSendNewMessage = async (message) => { + setInputState(InputState.SENDING); messageRef.current.value = ''; setDisableButton(true); @@ -297,14 +335,6 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { let quotedMessages = ''; if (quoteMessage.length > 0) { - // for (const quote of quoteMessage) { - // const { msg, attachments, _id } = quote; - // if (msg || attachments) { - // const msgLink = await getMessageLink(_id); - // quotedMessages += `[ ](${msgLink})`; - // } - // } - const quoteLinks = await Promise.all( quoteMessage.map(async (quote) => { const { msg, attachments, _id } = quote; @@ -330,17 +360,27 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { upsertMessage(pendingMessage, ECOptions.enableThreads); - const res = await RCInstance.sendMessage( - { - msg: pendingMessage.msg, - _id: pendingMessage._id, - }, - ECOptions.enableThreads ? threadId : undefined - ); + try { + const res = await RCInstance.sendMessage( + { + msg: pendingMessage.msg, + _id: pendingMessage._id, + }, + ECOptions.enableThreads ? threadId : undefined + ); - if (res.success) { - clearQuoteMessages(); - replaceMessage(pendingMessage, res.message); + if (res.success) { + clearQuoteMessages(); + replaceMessage(pendingMessage, res.message); + setInputState(InputState.IDLE); + } else { + setInputState(InputState.ERROR); + setTimeout(() => setInputState(InputState.IDLE), 3000); + } + } catch (e) { + console.error(e); + setInputState(InputState.ERROR); + setTimeout(() => setInputState(InputState.IDLE), 3000); } }; @@ -381,11 +421,16 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { messageRef.current.style.height = '44px'; const message = messageRef.current.value.trim(); - if (!message.length || !isUserAuthenticated) { + if (!isUserAuthenticated) { + return; + } + + if (!message.length && quoteMessage.length === 0) { messageRef.current.value = ''; if (editMessage.msg || editMessage.attachments) { setEditMessage({}); } + setDisableButton(true); return; } @@ -424,7 +469,12 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { sendTypingStart(); const message = val || e.target.value; messageRef.current.value = parseEmoji(message); - setDisableButton(!messageRef.current.value.length); + setDisableButton(!message.trim().length && !quoteMessage.length); + if (message.trim().length > 0) { + setInputState(InputState.DRAFTING); + } else { + setInputState(InputState.IDLE); + } if (e !== null) { handleNewLine(e, false); searchMentionUser(message); @@ -432,6 +482,12 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { } }; + useEffect(() => { + setDisableButton( + !messageRef.current?.value?.trim().length && !quoteMessage.length + ); + }, [quoteMessage, editMessage]); + const handleFocus = () => { if (chatInputContainer.current) { chatInputContainer.current.classList.add('focused'); @@ -449,11 +505,13 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { case e.ctrlKey && e.code === 'KeyI': { e.preventDefault(); formatSelection(messageRef, '_{{text}}_'); + onTextChange(null, messageRef.current.value); break; } case e.ctrlKey && e.code === 'KeyB': { e.preventDefault(); formatSelection(messageRef, '*{{text}}*'); + onTextChange(null, messageRef.current.value); break; } case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': @@ -595,6 +653,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { )} + { size="large" onClick={() => sendMessage()} type="primary" - disabled={disableButton || isRecordingMessage} - icon="send" - /> + disabled={ + disableButton || + isRecordingMessage || + inputState === InputState.SENDING + } + icon={ + inputState === InputState.SENDING + ? '' + : inputState === InputState.ERROR + ? 'cross' + : 'send' + } + > + {inputState === InputState.SENDING && +} + ) : null ) : ( {actions} ); }; diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 2447a9ebb4..d7eed01292 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -8,7 +8,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import { css } from '@emotion/react'; -import { EmbeddedChatApi } from '@embeddedchat/api'; +import { EmbeddedChatApi, MockAiAdapter } from '@embeddedchat/api'; import { Box, ToastBarProvider, @@ -102,7 +102,8 @@ const EmbeddedChat = (props) => { deleteToken, saveToken, }); - + // Initialize AI Adapter (Mock for now, can be configured via props later) + newRCInstance.setAiAdapter(new MockAiAdapter()); return newRCInstance; }, [host, roomId, getToken, deleteToken, saveToken]); diff --git a/packages/react/src/views/Message/Message.js b/packages/react/src/views/Message/Message.js index 81fb6ddada..20ecc7b407 100644 --- a/packages/react/src/views/Message/Message.js +++ b/packages/react/src/views/Message/Message.js @@ -130,17 +130,33 @@ const Message = ({ const handleStarMessage = async (msg) => { const isStarred = msg.starred && msg.starred.find((u) => u._id === authenticatedUserId); - if (!isStarred) { - await RCInstance.starMessage(msg._id); - dispatchToastMessage({ - type: 'success', - message: 'Message starred', - }); - } else { - await RCInstance.unstarMessage(msg._id); + const newStarred = isStarred + ? msg.starred.filter((u) => u._id !== authenticatedUserId) + : [...(msg.starred || []), { _id: authenticatedUserId }]; + + // Optimistic Update + upsertMessage({ ...msg, starred: newStarred }); + + try { + if (!isStarred) { + await RCInstance.starMessage(msg._id); + dispatchToastMessage({ + type: 'success', + message: 'Message starred', + }); + } else { + await RCInstance.unstarMessage(msg._id); + dispatchToastMessage({ + type: 'success', + message: 'Message unstarred', + }); + } + } catch (e) { + // Revert on error + upsertMessage(msg); dispatchToastMessage({ - type: 'success', - message: 'Message unstarred', + type: 'error', + message: 'Error updating star status', }); } getStarredMessages(); @@ -148,21 +164,28 @@ const Message = ({ const handlePinMessage = async (msg) => { const isPinned = msg.pinned; - msg.pinned = !isPinned; - const pinOrUnpin = isPinned - ? await RCInstance.unpinMessage(msg._id) - : await RCInstance.pinMessage(msg._id); - if (pinOrUnpin.error) { - msg.pinned = isPinned; - dispatchToastMessage({ - type: 'error', - message: 'Error pinning message', - }); - } else { + // Optimistic Update + upsertMessage({ ...msg, pinned: !isPinned }); + + try { + const pinOrUnpin = isPinned + ? await RCInstance.unpinMessage(msg._id) + : await RCInstance.pinMessage(msg._id); + + if (pinOrUnpin.error) { + throw new Error(pinOrUnpin.error); + } dispatchToastMessage({ type: 'success', message: isPinned ? 'Message unpinned' : 'Message pinned', }); + } catch (e) { + // Revert on error + upsertMessage(msg); + dispatchToastMessage({ + type: 'error', + message: 'Error updating pin status', + }); } }; @@ -231,6 +254,22 @@ const Message = ({ await RCInstance.reactToMessage(emoji, msg._id, canReact); }; + const handleTranslateMessage = async (msg) => { + try { + const translated = await RCInstance.translateMessage(msg.msg, 'en'); + dispatchToastMessage({ + type: 'info', + message: translated, + stay: true, + }); + } catch (e) { + dispatchToastMessage({ + type: 'error', + message: 'Error translating message', + }); + } + }; + const handleOpenThread = (msg) => async () => { openThread(msg); setShowSidebar(false); @@ -343,6 +382,7 @@ const Message = ({ setMessageToReport(message._id); toggleShowReportMessage(); }} + handleTranslateMessage={handleTranslateMessage} isThreadMessage={type === 'thread'} variantStyles={variantStyles} /> diff --git a/packages/react/src/views/Message/MessageToolbox.js b/packages/react/src/views/Message/MessageToolbox.js index bdac345674..187515ccf7 100644 --- a/packages/react/src/views/Message/MessageToolbox.js +++ b/packages/react/src/views/Message/MessageToolbox.js @@ -39,6 +39,7 @@ export const MessageToolbox = ({ handleCopyMessageLink, handleEditMessage, handleQuoteMessage, + handleTranslateMessage, isEditing = false, optionConfig = { surfaceItems: [ @@ -52,6 +53,7 @@ export const MessageToolbox = ({ "edit", "delete", "report", + "translate", ], menuItems: [], @@ -210,6 +212,13 @@ export const MessageToolbox = ({ visible: isAllowedToReport, type: "destructive", }, + translate: { + label: "Translate", + id: "translate", + onClick: () => handleTranslateMessage(message), + iconName: "language", + visible: RCInstance.getAiAdapter()?.enabled, + }, }), [ handleOpenThread, @@ -224,6 +233,7 @@ export const MessageToolbox = ({ handlerReportMessage, handleCopyMessage, handleCopyMessageLink, + handleTranslateMessage, isAllowedToPin, isAllowedToReport, isAllowedToEditMessage, diff --git a/temp_lerna_index.js b/temp_lerna_index.js new file mode 100644 index 0000000000..45f4e23395 --- /dev/null +++ b/temp_lerna_index.js @@ -0,0 +1,185 @@ +"use strict"; + +const os = require("os"); +const chalk = require("chalk"); +const execa = require("execa"); +const logTransformer = require("strong-log-transformer"); + +// bookkeeping for spawned processes +/** @type {Set>} */ +const children = new Set(); + +// when streaming processes are spawned, use this color for prefix +const colorWheel = ["cyan", "magenta", "blue", "yellow", "green", "blueBright"]; +const NUM_COLORS = colorWheel.length; + +// ever-increasing index ensures colors are always sequential +let currentColor = 0; + +/** + * Execute a command asynchronously, piping stdio by default. + * @param {string} command + * @param {string[]} args + * @param {import("execa").Options} [opts] + */ +function exec(command, args, opts) { + const options = Object.assign({ stdio: "pipe" }, opts); + const spawned = spawnProcess(command, args, options); + + return wrapError(spawned); +} + +/** + * Execute a command synchronously. + * @param {string} command + * @param {string[]} args + * @param {import("execa").SyncOptions} [opts] + */ +function execSync(command, args, opts) { + return execa.sync(command, args, opts).stdout; +} + +/** + * Spawn a command asynchronously, _always_ inheriting stdio. + * @param {string} command + * @param {string[]} args + * @param {import("execa").Options} [opts] + */ +function spawn(command, args, opts) { + const options = Object.assign({}, opts, { stdio: "inherit" }); + const spawned = spawnProcess(command, args, options); + + return wrapError(spawned); +} + +/** + * Spawn a command asynchronously, streaming stdio with optional prefix. + * @param {string} command + * @param {string[]} args + * @param {import("execa").Options} [opts] + * @param {string} [prefix] + */ +// istanbul ignore next +function spawnStreaming(command, args, opts, prefix) { + const options = Object.assign({}, opts); + options.stdio = ["ignore", "pipe", "pipe"]; + + const spawned = spawnProcess(command, args, options); + + const stdoutOpts = {}; + const stderrOpts = {}; // mergeMultiline causes escaped newlines :P + + if (prefix) { + const colorName = colorWheel[currentColor % NUM_COLORS]; + const color = chalk[colorName]; + + currentColor += 1; + + stdoutOpts.tag = `${color.bold(prefix)}:`; + stderrOpts.tag = `${color(prefix)}:`; + } + + // Avoid "Possible EventEmitter memory leak detected" warning due to piped stdio + if (children.size > process.stdout.listenerCount("close")) { + process.stdout.setMaxListeners(children.size); + process.stderr.setMaxListeners(children.size); + } + + spawned.stdout.pipe(logTransformer(stdoutOpts)).pipe(process.stdout); + spawned.stderr.pipe(logTransformer(stderrOpts)).pipe(process.stderr); + + return wrapError(spawned); +} + +function getChildProcessCount() { + return children.size; +} + +/** + * @param {import("execa").ExecaError } result + * @returns {number} + */ +function getExitCode(result) { + if (result.exitCode) { + return result.exitCode; + } + + // https://nodejs.org/docs/latest-v6.x/api/child_process.html#child_process_event_close + if (typeof result.code === "number") { + return result.code; + } + + // https://nodejs.org/docs/latest-v6.x/api/errors.html#errors_error_code + if (typeof result.code === "string") { + return os.constants.errno[result.code]; + } + + // we tried + return process.exitCode; +} + +/** + * @param {string} command + * @param {string[]} args + * @param {import("execa").Options} opts + */ +function spawnProcess(command, args, opts) { + const child = execa(command, args, opts); + + const drain = (exitCode, signal) => { + children.delete(child); + + // don't run repeatedly if this is the error event + if (signal === undefined) { + child.removeListener("exit", drain); + } + + // propagate exit code, if any + if (exitCode) { + process.exitCode = exitCode; + } + }; + + child.once("exit", drain); + child.once("error", drain); + + if (opts.pkg) { + child.pkg = opts.pkg; + } + + children.add(child); + + return child; +} + +/** + * @param {import("execa").ExecaChildProcess & { pkg?: import("@lerna/package").Package }} spawned + */ +function wrapError(spawned) { + if (spawned.pkg) { + return spawned.catch((err) => { + // ensure exit code is always a number + err.exitCode = getExitCode(err); + + // log non-lerna error cleanly + err.pkg = spawned.pkg; + + throw err; + }); + } + + return spawned; +} + +exports.exec = exec; +exports.execSync = execSync; +exports.spawn = spawn; +exports.spawnStreaming = spawnStreaming; +exports.getChildProcessCount = getChildProcessCount; +exports.getExitCode = getExitCode; + +/** + * @typedef {object} ExecOpts Provided to any execa-based call + * @property {string} cwd + * @property {number} [maxBuffer] + */ diff --git a/test_simple.js b/test_simple.js new file mode 100644 index 0000000000..6b2b3db0f6 --- /dev/null +++ b/test_simple.js @@ -0,0 +1 @@ +console.log("hello world"); From cb92782f6dc66b6d8b6d312f812e66b8c11336ec Mon Sep 17 00:00:00 2001 From: vivek Date: Wed, 25 Feb 2026 21:14:23 +0530 Subject: [PATCH 32/33] feat: enhance AI summary with rich Modal, improve ChatInput A11Y, and fix Typing Indicator stability --- packages/react/src/store/aiStore.js | 4 ++ .../src/views/ChatBody/AiSummaryModal.js | 40 +++++++++++++++++++ packages/react/src/views/ChatBody/ChatBody.js | 2 + .../react/src/views/ChatHeader/ChatHeader.js | 8 ++-- .../react/src/views/ChatInput/ChatInput.js | 15 +++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 packages/react/src/views/ChatBody/AiSummaryModal.js diff --git a/packages/react/src/store/aiStore.js b/packages/react/src/store/aiStore.js index d618105898..bf0c99399c 100644 --- a/packages/react/src/store/aiStore.js +++ b/packages/react/src/store/aiStore.js @@ -7,6 +7,10 @@ const useAiStore = create((set) => ({ setIsAiEnabled: (enabled) => set({ isAiEnabled: enabled }), aiLoading: false, setAiLoading: (loading) => set({ aiLoading: loading }), + isSummaryModalOpen: false, + setSummaryModalOpen: (open) => set({ isSummaryModalOpen: open }), + summaryContent: '', + setSummaryContent: (content) => set({ summaryContent: content }), })); export default useAiStore; diff --git a/packages/react/src/views/ChatBody/AiSummaryModal.js b/packages/react/src/views/ChatBody/AiSummaryModal.js new file mode 100644 index 0000000000..4dd88ff16a --- /dev/null +++ b/packages/react/src/views/ChatBody/AiSummaryModal.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { Modal, Icon, Button } from '@embeddedchat/ui-elements'; +import { Markdown } from '../Markdown'; +import useAiStore from '../../store/aiStore'; + +const AiSummaryModal = () => { + const { isSummaryModalOpen, setSummaryModalOpen, summaryContent } = useAiStore(); + + if (!isSummaryModalOpen) return null; + + const handleCopy = () => { + navigator.clipboard.writeText(summaryContent); + }; + + return ( + setSummaryModalOpen(false)}> + + ); +}; + +export default AiSummaryModal; diff --git a/packages/react/src/views/ChatBody/ChatBody.js b/packages/react/src/views/ChatBody/ChatBody.js index 531ede2a43..db3e583080 100644 --- a/packages/react/src/views/ChatBody/ChatBody.js +++ b/packages/react/src/views/ChatBody/ChatBody.js @@ -32,6 +32,7 @@ import RecentMessageButton from './RecentMessageButton'; import useFetchChatData from '../../hooks/useFetchChatData'; import { getChatbodyStyles } from './ChatBody.styles'; import UiKitModal from '../ModalBlock/uiKit/UiKitModal'; +import AiSummaryModal from './AiSummaryModal'; import useUiKitStore from '../../store/uiKitStore'; import useUiKitActionManager from '../../hooks/uiKit/useUiKitActionManager'; @@ -466,6 +467,7 @@ const ChatBody = ({ {uiKitModalOpen && (+ ++ ++ AI Conversation Summary + setSummaryModalOpen(false)} /> + + ++ + + + +)} + {popupVisible && otherUserMessage && ( diff --git a/packages/react/src/views/ChatHeader/ChatHeader.js b/packages/react/src/views/ChatHeader/ChatHeader.js index 54fa0be9b2..534a7e8032 100644 --- a/packages/react/src/views/ChatHeader/ChatHeader.js +++ b/packages/react/src/views/ChatHeader/ChatHeader.js @@ -24,6 +24,7 @@ import { useStarredMessageStore, useFileStore, useSidebarStore, + useAiStore, } from '../../store'; import { DynamicHeader } from '../DynamicHeader'; import useFetchChatData from '../../hooks/useFetchChatData'; @@ -460,11 +461,8 @@ const ChatHeader = ({ size="small" onClick={async () => { const summary = await RCInstance.getSummary(); - dispatchToastMessage({ - type: 'info', - message: summary, - stay: true, - }); + useAiStore.getState().setSummaryContent(summary); + useAiStore.getState().setSummaryModalOpen(true); }} title="Summarize thread" > diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index 908f64c442..b16b4c090b 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -198,6 +198,9 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { return () => { if (typeof unsubscribe === 'function') unsubscribe(); if (timerRef.current) clearTimeout(timerRef.current); + if (typingRef.current) { + RCInstance.sendTypingStatus(username, false).catch(console.error); + } }; }, [RCInstance, isChannelPrivate, setMembersHandler]); @@ -666,6 +669,18 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { Date: Wed, 25 Feb 2026 21:36:03 +0530 Subject: [PATCH 33/33] docs: add Pull Request guide and summary --- PULL_REQUEST_GUIDE.md | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 PULL_REQUEST_GUIDE.md diff --git a/PULL_REQUEST_GUIDE.md b/PULL_REQUEST_GUIDE.md new file mode 100644 index 0000000000..b23d495ec0 --- /dev/null +++ b/PULL_REQUEST_GUIDE.md @@ -0,0 +1,47 @@ +# Pull Request: AI Adapter Layer & Architecture Hardening + +## 🎯 Overview + +This PR implements a major architectural shift to satisfy the **GSoC 2026 Stability & AI Integration** requirements. It introduces a pluggable AI adapter layer and re-engineers the core authentication and input logic into deterministic **Finite State Machines (FSM)**. + +## 🚀 Key Features + +### 1. AI Adapter System + +- **Pluggable Interface**: Defined `IAiAdapter` for easy integration of any AI service. +- **Dynamic AI Store**: Managed state for AI-driven features (replies, summaries, loading states). +- **Contextual AI Tools**: + - **Smart Replies**: Suggested responses above the input field. + - **Message Translation**: Contextual translation in the message action bar. + - **Rich Summary Modal**: A premium `AiSummaryModal` for reading long conversation overviews. + +### 2. Architecture & Stability (FSM) + +- **Auth State Machine**: Transitions handled centrally in `RocketChatAuth.ts`. Fixes infinite loading states. +- **Message Input State Machine**: Explicit states (`DRAFTING`, `SENDING`, `ERROR`) to prevent race conditions and double-sends. +- **Hardware Cleanup**: Guaranteed release of camera/mic streams in `useMediaRecorder.js`. + +### 3. Accessibility (WCAG Compliance) + +- **Input Hardening**: Added ARIA labels and states to the main chat input to ensure screen reader compatibility. + +## 📝 Testing Instructions + +1. **AI Features**: Enable the `MockAiAdapter` in `EmbeddedChat.js`. + - Click "Summarize Thread" in any thread header. + - Use "Translate" in any message toolbox. + - Observe "Smart Replies" appear when new messages arrive. +2. **State Machines**: + - Force a login failure to see the `ERROR` state and automatic reset to `UNAUTHENTICATED`. + - Send a slow message to observe the `SENDING` state on the action button. +3. **Accessibility**: Use Chrome Vox or VoiceOver to navigate the message input and verify labels. + +## 🔗 Related Issues + +- Fulfills Milestone 1 & 2 of GSoC 2026 Proposal (Stability & AI). + +--- + +### [Link to Create Pull Request](https://github.com/RocketChat/EmbeddedChat/compare/develop...vivekyadav-3:feature/ai-adapter-complete) + +_(Note: Ensure base branch is set to `develop`)_