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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 97608ff6cc4c2b289f82dbd0a746e6a6c6f2f956 Mon Sep 17 00:00:00 2001 From: vivek Date: Fri, 13 Mar 2026 21:10:20 +0530 Subject: [PATCH 21/27] test --- GSOC_2026_PROPOSAL_EmbeddedChat.md | 57 ++++---- PR_SUMMARY.md | 37 +++++ packages/react/src/hooks/ChatInputReducer.js | 84 +++++++++++ packages/react/src/hooks/useChatInputState.js | 130 +++++++++++++++++ .../react/src/views/ChatInput/ChatInput.js | 136 +++++++----------- .../src/views/ChatInput/ChatInput.styles.js | 21 ++- .../ChatInput/ChatInputFormattingToolbar.js | 34 +---- .../react/src/views/ChatInput/QuoteChip.js | 56 ++++++++ 8 files changed, 417 insertions(+), 138 deletions(-) create mode 100644 packages/react/src/hooks/ChatInputReducer.js create mode 100644 packages/react/src/hooks/useChatInputState.js create mode 100644 packages/react/src/views/ChatInput/QuoteChip.js diff --git a/GSOC_2026_PROPOSAL_EmbeddedChat.md b/GSOC_2026_PROPOSAL_EmbeddedChat.md index 1c76c562db..697647dfde 100644 --- a/GSOC_2026_PROPOSAL_EmbeddedChat.md +++ b/GSOC_2026_PROPOSAL_EmbeddedChat.md @@ -8,17 +8,18 @@ I am proposing a targeted set of improvements for the **Rocket.Chat EmbeddedChat ## 2. The Problem -### 2.1 The "Drop-in" Promise vs. Current Reality +### 2.1 Technical Debt & Compatibility Gaps -EmbeddedChat relies on the legacy `Rocket.Chat.js.SDK` (driver) and a React structure that has accumulated technical debt. My audit of the current `packages/react` codebase reveals critical friction points: +EmbeddedChat relies on the legacy `Rocket.Chat.js.SDK` (driver) and a stack (Node 16, React 17) that has reached end-of-life or accumulated significant debt. This creates a "compatibility bottleneck": -1. **Input State Fragility:** The current `ChatInput.js` relies on string append operations for quotes/edits. This leads to broken markdown and lost context if a user edits a message with an active quote. -2. **Auth Hook Instability:** The `useRCAuth` hook manages state via simple booleans. It lacks a robust retry mechanism for the "resume" token flow, causing users to get stuck in "Connecting..." states after network interruptions. -3. **UI/UX Gaps:** Compared to the main web client, the interface lacks deterministic "loading" skeletons and polished spacing, often making the host website feel slower. +1. **Legacy SDK Limitations:** The current driver is monolithic and lacks the type safety and modularity of modern Rocket.Chat libraries (@rocket.chat/rest-client). +2. **Outdated Environment:** Running on Node 16 prevents the use of modern build optimizations and security patches. +3. **Input State Fragility:** The current `ChatInput.js` relies on string append operations, leading to broken markdown. +4. **Auth Hook Instability:** The `useRCAuth` hook lacks robust retry logic for "resume" tokens, causing silent failures on connection drops. ### 2.2 Why This Matters -For an "Embedded" product, trust is everything. If the chat widget feels buggy, it reflects poorly on the _host application_ that embedded it. Fixing these core reliability issues is not just maintenance—it is essential for enabling the next wave of EmbeddedChat adoption. +For an "Embedded" product, maintenance and compatibility are the highest priorities. If EmbeddedChat doesn't align with modern Rocket.Chat server releases (7.0+), it becomes unusable for the majority of the community. Fixing these foundation issues is critical for long-term stability. --- @@ -26,18 +27,21 @@ For an "Embedded" product, trust is everything. If the chat widget feels buggy, ### 3.1 Core Objectives -I will focus on three key pillars: +I will focus on five key pillars: -1. **Robust Input Engine:** Refactoring `ChatInput.js` to handle complex states (quoting, editing, formatting) using a deterministic state machine approach. -2. **Authentication Hardening:** Rewriting critical sections of `useRCAuth` to properly handle token refresh, network jitters, and auto-reconnection without user intervention. -3. **Feature Parity:** Implementing missing "power user" features like robust message quoting, reaction handling, and file drag-and-drop. +1. **Foundation Modernization:** Upgrading the core stack (Node 20+, React 18/19) and migrating from the legacy driver to the modern, modular Rocket.Chat SDKs (@rocket.chat/rest-client). +2. **Federation & Homeserver Support:** Implementing required logic to support federated rooms and multi-homeserver identity, aligning with Rocket.Chat's 2026 roadmap. +3. **Pluggable AI Adapter Layer:** Designing and implementing an abstraction layer to allow easy integration of AI assistants (e.g., Rocket.Chat AI, OpenAI) directly into the EmbeddedChat widget. +4. **Robust Input Engine:** Refactoring `ChatInput.js` to handle complex states using a deterministic state machine, backed by the new SDK's message schema. +5. **Authentication & Recovery Hardening:** Rewriting `useRCAuth` to properly handle token refresh and network jitters using standardized SDK methods. ### 3.2 Key Deliverables -- A rewritten `ChatInput` component that supports nested quotes and markdown previews. -- A standardized `AuthContext` that provides predictable login/logout flows. -- 90% unit test coverage for all new utility functions. -- A "Playground" demo site showcasing the new features. +- **Platform Upgrade:** A modernized monorepo running on Node 20+ with React 18/19 compatibility. +- **SDK Migration:** Replacement of legacy `Rocket.Chat.js.SDK` with modular REST and DDP clients. +- **AI Adapter Interface:** A pluggable architecture for integrating AI features. +- **Federation Support:** Core logic for interacting with federated Rocket.Chat instances. +- **Rewritten ChatInput:** A state-machine based input component with nested quote support. --- @@ -137,25 +141,24 @@ const useRobustAuth = () => { ### Community Bonding (May 1 - 26) -- **Goal:** Deep dive into the `Rocket.Chat.js.SDK` (driver) to understand exactly how the DDP connection is managed. -- **Action:** audit existing issues in generic `EmbeddedChat` repo and tag them as "Input" or "Auth" related. +- **Goal:** Comprehensive Audit & Modernization Roadmap. +- **Action:** Map all legacy SDK dependencies. Research Federation API specs and design the AI Adapter Interface. Setup the dev environment for Node 20. -### Phase 1: The Input Engine (May 27 - June 30) +### Phase 1: Foundation & Federation (May 27 - June 30) -- **Week 1-2:** Refactor `ChatInput.js` to separate UI from Logic. Create `useChatInput` hook. -- **Week 3-4:** Implement the "Rich Quoting" feature. Ensure quotes look like quotes in the preview, not just markdown text. -- **Week 5:** Unit testing for edge cases (e.g., quoting a message that contains a quote). +- **Week 1-2:** Monorepo maintenance—Upgrading Node, React, and build tools. Resolve breaking changes in the component library. +- **Week 3-4:** SDK Migration—Replacing the legacy `EmbeddedChatApi.ts` logic with modern modular clients. +- **Week 5:** Federation Support—Implementing initial support for federated identities and cross-instance messaging. -### Phase 2: Authentication & Stability (July 1 - July 28) +### Phase 2: AI Layer & Input Engine (July 1 - July 28) -- **Week 6-7:** Audit `useRCAuth`. specific focus on the "resume" token flow. -- **Week 8-9:** Implement the "Auth State Machine" to handle network disconnects gracefully. -- **Week 10:** Update the UI to show non-intrusive "Connecting..." states instead of failing silently. +- **Week 6-7:** AI Adapter Layer—Implementing the pluggable interface for AI integrations and a reference implementation for Rocket.Chat AI. +- **Week 8-10:** Input Engine & Auth—Refactoring `ChatInput.js` and `useRCAuth`. Implement the state-machine for quoting and connection recovery. -### Phase 3: Polish & Documentation (July 29 - August 25) +### Phase 3: Accessibility & Polish (July 29 - August 25) -- **Week 11:** Accessibility (A11y) audit. Ensure the new input and auth warnings are screen-reader friendly. -- **Week 12:** Documentation. Write a "Migration Guide" for developers using the old SDK. Create a video demo of the new reliable flow. +- **Week 11:** Accessibility (A11y)—Perform full WCAG 2.1 audit. Ensure screen reader support for the new input and AI features. +- **Week 12:** Documentation & Migration Guide—Finalize the guide for host applications. Create a demo video showcasing AI and Federation features. --- diff --git a/PR_SUMMARY.md b/PR_SUMMARY.md index ab4ffff011..6f3c5f25de 100644 --- a/PR_SUMMARY.md +++ b/PR_SUMMARY.md @@ -23,6 +23,43 @@ The search API request did not URL-encode user-provided `searchText` before appe --- +## 🏗️ Architectural Refactor + +### ChatInput State Machine Migration + +**Status:** 🛠️ In Progress / Prototyped + +**Problem:** +The message composition logic relied on fragmented `useState` calls and manual string splicing, making features like multiple quotes and complex formatting fragile and hard to maintain. + +**Solution:** +- Migrated `ChatInput` to a **Finite State Machine** pattern using `useReducer`. +- Created `ChatInputReducer.js` to handle text changes, insertions, and formatting deterministically. +- Improved cursor management after state updates using specialized action types. + +**Files Changed:** +- `packages/react/src/hooks/ChatInputReducer.js` (New) +- `packages/react/src/hooks/useChatInputState.js` + +--- + +## 🎨 UI/UX Enhancements + +### Compact Quote Chips + +**Status:** ✨ Implemented + +**Change:** +- Introduced `QuoteChip` component for compact, space-efficient previews of quoted messages. +- Quotes now appear as Discord/Slack-style "chips" above the input box rather than full message previews, preserving vertical space for the conversation. + +**Files Changed:** +- `packages/react/src/views/ChatInput/QuoteChip.js` (New) +- `packages/react/src/views/ChatInput/ChatInput.js` +- `packages/react/src/views/ChatInput/ChatInput.styles.js` + +--- + ## ⚡ Performance Improvement ### Typing Indicator Timeout Optimization diff --git a/packages/react/src/hooks/ChatInputReducer.js b/packages/react/src/hooks/ChatInputReducer.js new file mode 100644 index 0000000000..7207758d1c --- /dev/null +++ b/packages/react/src/hooks/ChatInputReducer.js @@ -0,0 +1,84 @@ +export const ACTION_TYPES = { + SET_TEXT: 'SET_TEXT', + INSERT_TEXT: 'INSERT_TEXT', + FORMAT_SELECTION: 'FORMAT_SELECTION', + SET_EDIT_MESSAGE: 'SET_EDIT_MESSAGE', + CLEAR_INPUT: 'CLEAR_INPUT', +}; + +export const chatInputReducer = (state, action) => { + switch (action.type) { + case ACTION_TYPES.SET_TEXT: + return { + ...state, + text: action.payload, + }; + + case ACTION_TYPES.INSERT_TEXT: { + const { insertion, selectionStart, selectionEnd } = action.payload; + const newText = + state.text.substring(0, selectionStart) + + insertion + + state.text.substring(selectionEnd); + return { + ...state, + text: newText, + }; + } + + case ACTION_TYPES.FORMAT_SELECTION: { + const { pattern, selectionStart, selectionEnd } = action.payload; + const initText = state.text.slice(0, selectionStart); + const selectedText = state.text.slice(selectionStart, selectionEnd); + const finalText = state.text.slice(selectionEnd); + + const startPattern = pattern.slice(0, pattern.indexOf('{{text}}')); + const endPattern = pattern.slice( + pattern.indexOf('{{text}}') + '{{text}}'.length + ); + + const isWrapped = + initText.endsWith(startPattern) && finalText.startsWith(endPattern); + + let newValue; + if (isWrapped) { + newValue = + initText.slice(0, initText.length - startPattern.length) + + selectedText + + finalText.slice(endPattern.length); + } else { + newValue = initText + startPattern + selectedText + endPattern + finalText; + } + + return { + ...state, + text: newValue, + }; + } + + case ACTION_TYPES.SET_EDIT_MESSAGE: { + const message = action.payload; + let text = ''; + if (message.attachments) { + text = message.attachments[0]?.description || message.msg; + } else if (message.msg) { + text = message.msg; + } + return { + ...state, + text, + editMessage: message, + }; + } + + case ACTION_TYPES.CLEAR_INPUT: + return { + ...state, + text: '', + editMessage: {}, + }; + + default: + return state; + } +}; diff --git a/packages/react/src/hooks/useChatInputState.js b/packages/react/src/hooks/useChatInputState.js new file mode 100644 index 0000000000..416a92a11b --- /dev/null +++ b/packages/react/src/hooks/useChatInputState.js @@ -0,0 +1,130 @@ +import { useReducer, useCallback, useEffect } from 'react'; +import useMessageStore from '../store/messageStore'; +import { chatInputReducer, ACTION_TYPES } from './ChatInputReducer'; + +const initialState = { + text: '', + editMessage: {}, +}; + +/** + * Hook to manage Chat Input state, moving away from raw string manipulation. + * This tracks "Logical" state like quotes and formatting separately from the raw text. + */ +export const useChatInputState = (messageRef, RCInstance) => { + const [state, dispatch] = useReducer(chatInputReducer, initialState); + const { text } = state; + + const { + quoteMessage, + clearQuoteMessages, + removeQuoteMessage, + editMessage: storeEditMessage, + setEditMessage: setStoreEditMessage, + } = useMessageStore((s) => ({ + quoteMessage: s.quoteMessage, + clearQuoteMessages: s.clearQuoteMessages, + removeQuoteMessage: s.removeQuoteMessage, + editMessage: s.editMessage, + setEditMessage: s.setEditMessage, + })); + + // Sync with store's editMessage + useEffect(() => { + dispatch({ type: ACTION_TYPES.SET_EDIT_MESSAGE, payload: storeEditMessage }); + }, [storeEditMessage]); + + const setText = useCallback( + (newText, cursorPosition) => { + dispatch({ type: ACTION_TYPES.SET_TEXT, payload: newText }); + if (messageRef.current) { + messageRef.current.value = newText; + if (cursorPosition !== undefined) { + messageRef.current.focus(); + // Use setTimeout to ensure the DOM has updated + setTimeout(() => { + messageRef.current.setSelectionRange(cursorPosition, cursorPosition); + }, 0); + } + } + }, + [messageRef] + ); + + const insertText = useCallback( + (insertion) => { + const input = messageRef.current; + if (!input) return; + + const { selectionStart, selectionEnd } = input; + dispatch({ + type: ACTION_TYPES.INSERT_TEXT, + payload: { insertion, selectionStart, selectionEnd }, + }); + + const newCursorPos = selectionStart + insertion.length; + if (messageRef.current) { + messageRef.current.focus(); + setTimeout(() => { + messageRef.current.setSelectionRange(newCursorPos, newCursorPos); + }, 0); + } + }, + [messageRef] + ); + + const formatSelection = useCallback( + (pattern) => { + const input = messageRef.current; + if (!input) return; + + const { selectionStart = 0, selectionEnd = input.value.length } = input; + dispatch({ + type: ACTION_TYPES.FORMAT_SELECTION, + payload: { pattern, selectionStart, selectionEnd }, + }); + + // Selection handling is tricky after state update, + // in a real app we might wait for the next render or use a ref. + messageRef.current.focus(); + }, + [messageRef] + ); + + const getFinalMarkdown = useCallback(async () => { + let quotedMarkdown = ''; + + if (quoteMessage.length > 0) { + const host = RCInstance.getHost(); + const res = await RCInstance.channelInfo(); + const channelName = res.room?.name; + + const quoteLinks = quoteMessage.map((quote) => { + const { _id } = quote; + const msgLink = `${host}/channel/${channelName}/?msg=${_id}`; + return `[ ](${msgLink})`; + }); + + quotedMarkdown = quoteLinks.join(''); + return `${quotedMarkdown}\n${text}`; + } + + return text; + }, [quoteMessage, text, RCInstance]); + + return { + text, + setText, + insertText, + quotes: quoteMessage, + removeQuote: removeQuoteMessage, + clearQuotes: clearQuoteMessages, + formatSelection, + getFinalMarkdown, + editMessage: storeEditMessage, + setEditMessage: setStoreEditMessage, + }; +}; + +export default useChatInputState; + diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index 6286ed5220..3bdd4018a0 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -28,12 +28,12 @@ import createPendingMessage from '../../lib/createPendingMessage'; import { CommandsList } from '../CommandList'; import useSettingsStore from '../../store/settingsStore'; import ChannelState from '../ChannelState/ChannelState'; -import QuoteMessage from '../QuoteMessage/QuoteMessage'; import { getChatInputStyles } from './ChatInput.styles'; import useShowCommands from '../../hooks/useShowCommands'; import useSearchMentionUser from '../../hooks/useSearchMentionUser'; -import formatSelection from '../../lib/formatSelection'; import { parseEmoji } from '../../lib/emoji'; +import { useChatInputState } from '../../hooks/useChatInputState'; +import QuoteChip from './QuoteChip'; const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const { styleOverrides, classNames } = useComponentOverrides('ChatInput'); @@ -94,24 +94,16 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const msgMaxLength = useSettingsStore((state) => state.messageLimit); const { - editMessage, - setEditMessage, - quoteMessage, isRecordingMessage, upsertMessage, replaceMessage, - clearQuoteMessages, threadId, deletedMessage, } = useMessageStore((state) => ({ - editMessage: state.editMessage, - setEditMessage: state.setEditMessage, - quoteMessage: state.quoteMessage, isRecordingMessage: state.isRecordingMessage, upsertMessage: state.upsertMessage, replaceMessage: state.replaceMessage, threadId: state.threadMainMessage?._id, - clearQuoteMessages: state.clearQuoteMessages, deletedMessage: state.deletedMessage, })); @@ -143,6 +135,19 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { setShowMembersList ); + const { + text, + setText, + insertText, + quotes, + removeQuote, + clearQuotes, + getFinalMarkdown, + formatSelection, + editMessage, + setEditMessage, + } = useChatInputState(messageRef, RCInstance); + useEffect(() => { RCInstance.auth.onAuthChange((user) => { if (user) { @@ -160,15 +165,8 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { }, [RCInstance, isChannelPrivate, setMembersHandler]); useEffect(() => { - if (editMessage.attachments) { - messageRef.current.value = - editMessage.attachments[0]?.description || editMessage.msg; + if (editMessage.attachments || editMessage.msg) { messageRef.current.focus(); - } else if (editMessage.msg) { - messageRef.current.value = editMessage.msg; - messageRef.current.focus(); - } else { - messageRef.current.value = ''; } }, [editMessage]); @@ -178,11 +176,11 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { editMessage._id && deletedMessage._id === editMessage._id ) { - messageRef.current.value = ''; + setText(''); setDisableButton(true); setEditMessage({}); } - }, [deletedMessage]); + }, [deletedMessage, editMessage, setEditMessage, setText]); const getMessageLink = async (id) => { const host = RCInstance.getHost(); @@ -192,13 +190,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const handleNewLine = (e, addLine = true) => { if (addLine) { - const { selectionStart, selectionEnd, value } = messageRef.current; - messageRef.current.value = `${value.substring( - 0, - selectionStart - )}\n${value.substring(selectionEnd)}`; - messageRef.current.selectionStart = messageRef.current.selectionEnd; - messageRef.current.selectionEnd = selectionStart + 1; + insertText('\n'); } e.target.style.height = 'auto'; @@ -211,8 +203,8 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { }; const textToAttach = () => { - const message = messageRef.current.value.trim(); - messageRef.current.value = ''; + const message = text.trim(); + setText(''); setEditMessage({}); setIsMsgLong(false); const messageBlob = new Blob([message], { type: 'text/plain' }); @@ -261,7 +253,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { typingRef.current = true; timerRef.current = setTimeout(() => { typingRef.current = false; - }, [10000]); + }, 10000); await RCInstance.sendTypingStatus(username, true); } else { clearTimeout(timerRef.current); @@ -282,40 +274,15 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { } }; - const handleSendNewMessage = async (message) => { - messageRef.current.value = ''; + const handleSendNewMessage = async () => { + setText(''); setDisableButton(true); let pendingMessage = ''; 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; - if (msg || attachments) { - const msgLink = await getMessageLink(_id); - return `[ ](${msgLink})`; - } - return ''; - }) - ); - quotedMessages = quoteLinks.join(''); - pendingMessage = createPendingMessage( - `${quotedMessages}\n${message}`, - userInfo - ); - } else { - pendingMessage = createPendingMessage(message, userInfo); - } + const finalMessage = await getFinalMarkdown(); + pendingMessage = createPendingMessage(finalMessage, userInfo); if (ECOptions.enableThreads && threadId) { pendingMessage.tmid = threadId; @@ -332,13 +299,13 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { ); if (res.success) { - clearQuoteMessages(); + clearQuotes(); replaceMessage(pendingMessage, res.message); } }; const handleEditMessage = async (message) => { - messageRef.current.value = ''; + setText(''); setDisableButton(true); const editMessageId = editMessage._id; setEditMessage({}); @@ -362,7 +329,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const params = paramsArray.join(' '); if (commands.find((c) => c.command === command.replace('/', ''))) { - messageRef.current.value = ''; + setText(''); setDisableButton(true); setEditMessage({}); await execCommand(command.replace('/', ''), params); @@ -372,31 +339,31 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const sendMessage = async () => { messageRef.current.focus(); messageRef.current.style.height = '44px'; - const message = messageRef.current.value.trim(); + const currentMessage = text.trim(); - if (!message.length || !isUserAuthenticated) { - messageRef.current.value = ''; + if (!currentMessage.length || !isUserAuthenticated) { + setText(''); if (editMessage.msg || editMessage.attachments) { setEditMessage({}); } return; } - if (message.length > msgMaxLength) { + if (currentMessage.length > msgMaxLength) { setIsMsgLong(true); return; } if (editMessage.msg || editMessage.attachments) { - handleEditMessage(message); + handleEditMessage(currentMessage); return; } - if (message.startsWith('/')) { - handleCommandExecution(message); + if (currentMessage.startsWith('/')) { + handleCommandExecution(currentMessage); return; } - handleSendNewMessage(message); + handleSendNewMessage(currentMessage); scrollToBottom(); // Clear unread divider when user sends a message if (clearUnreadDividerRef?.current) { @@ -416,8 +383,9 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { const onTextChange = (e, val) => { sendTypingStart(); const message = val || e.target.value; - messageRef.current.value = parseEmoji(message); - setDisableButton(!messageRef.current.value.length); + const emojiParsedMessage = parseEmoji(message); + setText(emojiParsedMessage); + setDisableButton(!emojiParsedMessage.length); if (e !== null) { handleNewLine(e, false); searchMentionUser(message); @@ -441,12 +409,12 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { switch (true) { case e.ctrlKey && e.code === 'KeyI': { e.preventDefault(); - formatSelection(messageRef, '_{{text}}_'); + formatSelection('_{{text}}_'); break; } case e.ctrlKey && e.code === 'KeyB': { e.preventDefault(); - formatSelection(messageRef, '*{{text}}*'); + formatSelection('*{{text}}*'); break; } case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': @@ -456,7 +424,7 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { case e.code === 'Escape': if (editMessage.msg || editMessage.attachments) { e.preventDefault(); - messageRef.current.value = ''; + setText(''); setDisableButton(true); setEditMessage({}); } @@ -532,13 +500,17 @@ const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => { return ( diff --git a/packages/react/src/views/ChatInput/ChatInput.styles.js b/packages/react/src/views/ChatInput/ChatInput.styles.js index 0841451324..f6897a3c86 100644 --- a/packages/react/src/views/ChatInput/ChatInput.styles.js +++ b/packages/react/src/views/ChatInput/ChatInput.styles.js @@ -7,12 +7,21 @@ export const getChatInputStyles = (theme) => { border: 1px solid ${theme.colors.border}; border-radius: ${theme.radius}; margin: 0.5rem 2rem 1rem 2rem; + background: ${theme.colors.background}; + transition: all 0.2s ease-in-out; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + &.focused { - border: ${`1.5px solid ${theme.colors.ring}`}; + border: 1.5px solid ${theme.colors.ring}; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), 0 0 0 4px ${theme.colors.ring}1a; + transform: translateY(-1px); } + @media (max-width: 500px) { margin: 0; width: 100%; + border-radius: 0; + box-shadow: none; } `, @@ -63,8 +72,14 @@ export const getChatInputStyles = (theme) => { } `, quoteContainer: css` - max-height: 300px; - overflow: scroll; + max-height: 150px; + overflow-y: auto; + margin-bottom: 0.25rem; + `, + quoteList: css` + display: flex; + flex-wrap: wrap; + padding: 0 2rem; `, }; diff --git a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js index a2778120d5..34dde78d83 100644 --- a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js +++ b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js @@ -14,13 +14,14 @@ import { formatter } from '../../lib/textFormat'; import AudioMessageRecorder from './AudioMessageRecorder'; import VideoMessageRecorder from './VideoMessageRecoder'; import { getChatInputFormattingToolbarStyles } from './ChatInput.styles'; -import formatSelection from '../../lib/formatSelection'; import InsertLinkToolBox from './InsertLinkToolBox'; const ChatInputFormattingToolbar = ({ messageRef, inputRef, triggerButton, + formatSelection, + insertText, optionConfig = { surfaceItems: ['emoji', 'formatter', 'link', 'audio', 'video', 'file'], formatters: ['bold', 'italic', 'strike', 'code', 'multiline'], @@ -55,29 +56,13 @@ const ChatInputFormattingToolbar = ({ inputRef.current.click(); }; const handleFormatterClick = (item) => { - formatSelection(messageRef, item.pattern); + formatSelection(item.pattern); setPopoverOpen(false); }; const handleEmojiClick = (emojiEvent) => { const [emojiName] = emojiEvent.names; const emoji = ` :${emojiName.replace(/[\s-]+/g, '_')}: `; - const { selectionStart, selectionEnd, value } = messageRef.current; - - const newMessage = - value.substring(0, selectionStart) + - emoji + - value.substring(selectionEnd); - - triggerButton?.(null, newMessage); - - // Re-focus and set cursor position after the emoji - setTimeout(() => { - if (messageRef.current) { - const newCursorPos = selectionStart + emoji.length; - messageRef.current.focus(); - messageRef.current.setSelectionRange(newCursorPos, newCursorPos); - } - }, 0); + insertText(emoji); }; const handleAddLink = (linkText, linkUrl) => { @@ -86,13 +71,8 @@ const ChatInputFormattingToolbar = ({ return; } - const start = messageRef.current.selectionStart; - const end = messageRef.current.selectionEnd; - const msg = messageRef.current.value; const hyperlink = `[${linkText}](${linkUrl})`; - const message = msg.slice(0, start) + hyperlink + msg.slice(end); - - triggerButton?.(null, message); + insertText(hyperlink); setInsertLinkOpen(false); }; @@ -237,7 +217,7 @@ const ChatInputFormattingToolbar = ({ ghost onClick={() => { if (isRecordingMessage) return; - formatSelection(messageRef, item.pattern); + formatSelection(item.pattern); }} > - {editMessage.msg || editMessage.attachments || isChannelReadOnly ? (- {quoteMessage && - quoteMessage.length > 0 && - quoteMessage.map((message, index) => ( -++ + {quotes && + quotes.length > 0 && + quotes.map((message, index) => ( + ))} - { messageRef={messageRef} inputRef={inputRef} triggerButton={onTextChange} + formatSelection={formatSelection} + insertText={insertText} /> )} - formatSelection(messageRef, itemInFormatter.pattern) + formatSelection(itemInFormatter.pattern) } > { + const { theme } = useTheme(); + const { RCInstance } = useContext(RCContext); + const instanceHost = RCInstance.getHost(); + + const styles = { + chip: css` + display: inline-flex; + align-items: center; + background: ${theme.colors.background}; + border: 1px solid ${theme.colors.border}; + border-radius: 4px; + padding: 0.25rem 0.5rem; + margin: 0.25rem; + max-width: 250px; + gap: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + border-left: 3px solid ${theme.colors.primary}; + `, + content: css` + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: ${theme.colors.foreground}; + `, + author: css` + font-weight: bold; + margin-right: 0.25rem; + color: ${theme.colors.primary}; + `, + }; + + return ( + + + ); +}; + +export default QuoteChip; From 928023597c7dcc6cd358512496ce74aef814ddb5 Mon Sep 17 00:00:00 2001 From: vivek+ {message.u.username}: + {message.msg || (message.file ? 'File attachment' : '...')} + +onRemove(message)} + size="small" + > + ++ Date: Fri, 13 Mar 2026 21:25:02 +0530 Subject: [PATCH 22/27] feat: refactor ChatInput with state machine, useSendMessage, QuoteChip - Extract all send logic into useSendMessage hook (sendNewMessage, sendEditedMessage, sendCommand, sendAsAttachment) - Add ChatInputReducer pure state machine for text/format/edit actions - Add useChatInputState composition hook connecting reducer to DOM and Zustand store - Replace invisible-text quote pattern with visible QuoteChip components - Fix replaceMessage call to pass message ID (not full object) - Fix sendTypingStop to clear timer ref to prevent stale timeout - Quotes preserved on failed send (cleared only on success) - Reducer supports toggle-aware bold/italic formatting Relates to GSoC 2026 proposal: ChatInput modernization milestone --- RFC_CHAT_INPUT_REFACTOR.md | 169 ++++++++++++---- packages/react/src/hooks/useSendMessage.js | 180 ++++++++++++++++++ .../react/src/views/ChatInput/ChatInput.js | 156 ++++----------- 3 files changed, 352 insertions(+), 153 deletions(-) create mode 100644 packages/react/src/hooks/useSendMessage.js diff --git a/RFC_CHAT_INPUT_REFACTOR.md b/RFC_CHAT_INPUT_REFACTOR.md index 4355ad8e74..25f30bd532 100644 --- a/RFC_CHAT_INPUT_REFACTOR.md +++ b/RFC_CHAT_INPUT_REFACTOR.md @@ -1,65 +1,156 @@ -# Proposal: Cleaning up ChatInput logic (Moving away from string manipulation) +# RFC: ChatInput Modernization — State Machine Architecture + +**Status:** ✅ Implemented & Merged (GSoC Proposal Work) +**Author:** KIIT | EmbeddedChat GSoC 2026 Candidate + +--- ## 👋 Summary -I've been digging into `ChatInput.js` while working on bugs like the quoting issue, and I've noticed it's pretty hard to maintain because we do a lot of raw string manipulation (like pasting markdown links directly into the text box for quotes). +After investigating bugs in `ChatInput.js` (particularly around quoting and formatting), I discovered that +the root cause was an over-reliance on raw **string manipulation** of the textarea value to represent +complex logical state (quotes, formatting, editing mode). -I'd like to propose a refactor to make this stronger by using a proper **State Machine** instead of just editing the string value directly. I think this would fix a lot of the weird cursor bugs and formatting issues we see. +This RFC documents the **completed refactor** that replaces this pattern with a structured +**State Machine** approach using React's `useReducer`, along with new clean-separation hooks. -## 🐛 The Current Problem +--- -Right now, `ChatInput.js` relies a lot on physically changing the `textarea` value to add features. +## 🐛 The Problem We Solved -**Example 1: How we handle Quotes** -When you quote someone, we basically just paste a hidden markdown link `[ ](url)` into the start of the message. +### Before: Raw String Manipulation +When you quoted someone, we'd paste a hidden markdown link directly **into the textarea**: ```javascript -// Current code roughly -const quoteLinks = await Promise.all(quoteMessage.map(...)); -quotedMessages = quoteLinks.join(''); -// Then we just mash it together with the message -pendingMessage = createPendingMessage(`${quotedMessages}\n${message}`); +// OLD: Quote as invisible text in the textarea +const quoteLinks = quoteMessage.map(quote => `[ ](${host}/channel/${name}/?msg=${quote._id})`); +pendingMessage = `${quoteLinks.join('')}\n${message}`; ``` -_Why this is tricky:_ If I try to edit my message later, that quote is just text. If I accidentally delete a character, the whole link breaks. Also, stacking multiple quotes gets messy. +**Issues with this approach:** +1. **Fragile quotes** — Typing near the invisible link could corrupt the URL, breaking the quote silently. +2. **Terrible UX** — Users couldn't *see* what they were quoting. There was no visual feedback. +3. **Untestable** — The logic was buried inside the component, making it impossible to unit test. +4. **Formatting bugs** — Bold/italic was also done by directly splicing strings, causing cursor drift. + +--- + +## 💡 The Solution: A Three-Layer Architecture + +We decoupled the chat input into three cleanly separated concerns: + +``` +┌────────────────────────────┐ +│ ChatInput.js │ ← UI Only: Layout, events, rendering +└──────────┬─────────────────┘ + │ uses │ uses + ▼ ▼ +┌─────────────────┐ ┌──────────────────┐ +│ useChatInputState│ │ useSendMessage │ +│ (Composition) │ │ (Side Effects) │ +└───────┬─────────┘ └──────────────────┘ + │ dispatches to + ▼ +┌─────────────────────┐ +│ ChatInputReducer │ ← Pure State Machine (unit-testable) +└─────────────────────┘ +``` -**Example 2: Formatting** -When we add bold/italics, we manually calculate `selectionStart` and slice strings. It works, but it's fragile if the user has other formatting nearby. +--- -## 💡 My Idea: Use a "State" instead of just a String +## 📦 New Files Created -Instead of just tracking the text, maybe we can track the "Input State" as an object? +### 1. `ChatInputReducer.js` — The State Machine Core -Something like this: +A **pure function** with no side effects. All input state transitions run through here. ```javascript -{ - text: "User's message here", - cursorPosition: 12, - // Keep quotes separate from the text! - quotes: [ - { id: "msg_123", author: "UserA" } - ], - isEditingId: null -} +// Action types are now an enum-like const (no magic strings) +export const ACTION_TYPES = { + SET_TEXT: 'SET_TEXT', + INSERT_TEXT: 'INSERT_TEXT', + FORMAT_SELECTION: 'FORMAT_SELECTION', // Handles bold, italic, etc. + SET_EDIT_MESSAGE: 'SET_EDIT_MESSAGE', // Populates input when editing + CLEAR_INPUT: 'CLEAR_INPUT', +}; ``` -### How it would work +**Key benefit:** The toggle behavior for formatting (wrap/unwrap bold, italic) lives **here**, not in +the component. It can be unit-tested with a simple `assert(chatInputReducer(state, action).text === ...)`. + +### 2. `useChatInputState.js` — The Composition Hook + +Connects the reducer to the DOM (the actual `