@@ -9,6 +9,7 @@ import GroupUserList from '../group/GroupUserList.tsx'
99import ChatInput from '../input/ChatInput.tsx'
1010import MessageList from './MessageList.tsx'
1111import { GroupChat } from '@/types/chat.ts'
12+ import { cleanRoomId } from '@/utils/roomUtils.ts'
1213
1314// 动画常量
1415const overlayShow = 'animate-[overlay-show_150ms_cubic-bezier(0.16,1,0.3,1)]' ;
@@ -23,12 +24,17 @@ const ChatPanel: React.FC = () => {
2324 const joinGroupChat = useChatStore ( state => state . joinGroupChat ) ;
2425 const pendingRoomId = useChatStore ( state => state . pendingRoomId ) ;
2526 const isPeerInitialized = useChatStore ( state => state . isPeerInitialized ) ;
27+ const chats = useChatStore ( state => state . chats ) ;
28+ const setCurrentChat = useChatStore ( state => state . setCurrentChat ) ;
2629
2730 const [ nameDialogOpen , setNameDialogOpen ] = useState ( false ) ;
2831 const [ tempUserName , setTempUserName ] = useState ( '' ) ;
2932 const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
30- const [ isLocalNetwork , setIsLocalNetwork ] = useState < boolean | null > ( null ) ;
33+ const [ isLocalNetwork ] = useState < boolean | null > ( null ) ;
3134 const [ networkModeDialogOpen , setNetworkModeDialogOpen ] = useState ( false ) ;
35+ const [ joinDialogOpen , setJoinDialogOpen ] = useState ( false ) ;
36+ const [ roomIdInput , setRoomIdInput ] = useState ( '' ) ;
37+ const [ isJoining , setIsJoining ] = useState ( false ) ;
3238
3339 // 首次加载时检查是否已设置用户名
3440 useEffect ( ( ) => {
@@ -59,6 +65,9 @@ const ChatPanel: React.FC = () => {
5965 const handleError = ( message : string ) => {
6066 setErrorMessage ( message ) ;
6167
68+ // 重置加入群聊的状态
69+ setIsJoining ( false ) ;
70+
6271 // 检查是否是连接错误
6372 if ( message . includes ( 'Could not connect to peer' ) ) {
6473 // 提取对等节点ID
@@ -87,13 +96,18 @@ const ChatPanel: React.FC = () => {
8796 } , 5000 ) ;
8897 } ;
8998
90- const handleGroupCreated = ( data ?: { isLocalNetwork ?: boolean } ) => {
99+ const handleGroupCreated = ( _data ?: { isLocalNetwork ?: boolean } ) => {
91100 toast . success ( '群聊创建成功' ) ;
92101 } ;
93102
94103 const handleJoinedGroup = ( groupChat ?: GroupChat ) => {
95104 toast . dismiss ( 'connecting' ) ; // 清除连接中的提示
96105
106+ // 重置加入群聊的状态
107+ setJoinDialogOpen ( false ) ;
108+ setRoomIdInput ( '' ) ;
109+ setIsJoining ( false ) ;
110+
97111 if ( groupChat ) {
98112 toast . success (
99113 < div >
@@ -174,6 +188,82 @@ const ChatPanel: React.FC = () => {
174188 setNetworkModeDialogOpen ( false ) ;
175189 } ;
176190
191+ const handleJoinGroupChat = ( ) => {
192+ if ( ! roomIdInput . trim ( ) ) {
193+ toast . error ( '请输入有效的群聊ID或链接' ) ;
194+ return ;
195+ }
196+
197+ if ( ! userName ) {
198+ setNameDialogOpen ( true ) ;
199+ return ;
200+ }
201+
202+ setIsJoining ( true ) ;
203+
204+ // 显示正在连接的提示
205+ toast . loading ( `正在连接到群聊...` , {
206+ id : 'connecting' ,
207+ duration : 20000 // 设置较长的持续时间,避免自动消失
208+ } ) ;
209+
210+ // 使用工具函数清理输入
211+ const cleanedRoomId = cleanRoomId ( roomIdInput ) ;
212+
213+ // 检查是否已经加入了该群聊
214+ const existingChat = chats . find ( chat =>
215+ chat . isGroup && ( chat as GroupChat ) . roomId === cleanedRoomId
216+ ) ;
217+
218+ if ( existingChat ) {
219+ toast . dismiss ( 'connecting' ) ;
220+ toast . success ( '已经加入过该群聊,直接切换' ) ;
221+ setCurrentChat ?.( existingChat ) ;
222+ setJoinDialogOpen ( false ) ;
223+ setRoomIdInput ( '' ) ;
224+ setIsJoining ( false ) ;
225+ return ;
226+ }
227+
228+ // 加入群聊
229+ joinGroupChat ?.( cleanedRoomId ) ;
230+ } ;
231+
232+ const handleJoinFromUrl = ( ) => {
233+ processUrlInput ( ) ;
234+ } ;
235+
236+ const processUrlInput = ( ) => {
237+ try {
238+ // 检查是否是URL
239+ if ( roomIdInput . startsWith ( 'http' ) ) {
240+ const url = new URL ( roomIdInput ) ;
241+ const roomIdParam = url . searchParams . get ( 'roomId' ) ;
242+
243+ if ( roomIdParam ) {
244+ // 更新输入框显示提取出的roomId
245+ setRoomIdInput ( roomIdParam ) ;
246+ toast . success ( '已从链接中提取群聊ID' ) ;
247+ } else {
248+ toast . error ( '无法从链接中提取群聊ID' ) ;
249+ }
250+ } else {
251+ // 如果不是URL,尝试直接作为roomId处理
252+ handleJoinGroupChat ( ) ;
253+ }
254+ } catch ( error ) {
255+ console . error ( '处理URL时出错:' , error ) ;
256+ toast . error ( '无效的链接格式' ) ;
257+ }
258+ } ;
259+
260+ // 处理回车键提交
261+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
262+ if ( e . key === 'Enter' && ! isJoining ) {
263+ handleJoinGroupChat ( ) ;
264+ }
265+ } ;
266+
177267 if ( ! currentChat ) {
178268 return (
179269 < div className = "h-full flex flex-col items-center justify-center space-y-4 p-4" >
@@ -233,7 +323,7 @@ const ChatPanel: React.FC = () => {
233323 ) }
234324 </ div >
235325
236- < div className = "flex space-x-4 " >
326+ < div className = "flex flex-wrap gap-4 justify-center " >
237327 < button
238328 onClick = { handleCreateGroupChat }
239329 disabled = { isConnecting }
@@ -249,22 +339,20 @@ const ChatPanel: React.FC = () => {
249339 < span > 创建群聊</ span >
250340 </ button >
251341
252- { userName && (
253- < button
254- onClick = { ( ) => setNameDialogOpen ( true ) }
255- disabled = { isConnecting }
256- className = { `px-6 py-3 bg-gray-100 text-gray-700 rounded-lg
257- transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl
258- ${ isConnecting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-200' } ` }
259- >
260- < svg className = "w-5 h-5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
261- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 }
262- d = "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
263- />
264- </ svg >
265- < span > 修改用户名</ span >
266- </ button >
267- ) }
342+ < button
343+ onClick = { ( ) => setJoinDialogOpen ( true ) }
344+ disabled = { isConnecting }
345+ className = { `px-6 py-3 bg-green-500 text-white rounded-lg
346+ transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl
347+ ${ isConnecting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-green-600' } ` }
348+ >
349+ < svg className = "w-5 h-5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
350+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 }
351+ d = "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
352+ />
353+ </ svg >
354+ < span > 加入群聊</ span >
355+ </ button >
268356 </ div >
269357
270358 { /* 用户名输入对话框 */ }
@@ -324,6 +412,73 @@ const ChatPanel: React.FC = () => {
324412 </ Portal >
325413 </ Root >
326414
415+ { /* 加入群聊对话框 */ }
416+ < Root open = { joinDialogOpen } onOpenChange = { setJoinDialogOpen } >
417+ < Portal >
418+ < Overlay className = { `fixed inset-0 bg-black/30 ${ overlayShow } ` } />
419+ < Content
420+ className = { `fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]
421+ w-[90vw] max-w-[450px] rounded-lg bg-white p-6 shadow-xl focus:outline-none
422+ ${ contentShow } ` }
423+ >
424+ < Title className = "text-xl font-semibold mb-4" > 加入群聊</ Title >
425+ < Description className = "text-gray-500 mb-4" >
426+ 请输入群聊ID或邀请链接:
427+ </ Description >
428+ < div className = "mb-4" >
429+ < input
430+ type = "text"
431+ value = { roomIdInput }
432+ onChange = { ( e ) => setRoomIdInput ( e . target . value ) }
433+ onKeyDown = { handleKeyDown }
434+ placeholder = "输入群聊ID或粘贴邀请链接"
435+ className = "w-full px-3 py-2 border border-gray-300 rounded-md mb-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
436+ autoFocus
437+ disabled = { isJoining }
438+ />
439+ < div className = "flex justify-between" >
440+ < button
441+ onClick = { handleJoinFromUrl }
442+ disabled = { isJoining || ! roomIdInput . trim ( ) }
443+ className = { `text-sm text-blue-500 hover:text-blue-600
444+ ${ ( isJoining || ! roomIdInput . trim ( ) ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
445+ >
446+ 从链接提取ID
447+ </ button >
448+ < div className = "text-xs text-gray-500" >
449+ 例如: abc123 或 https://example.com?roomId=abc123
450+ </ div >
451+ </ div >
452+ </ div >
453+ < div className = "flex justify-end space-x-2" >
454+ < button
455+ onClick = { ( ) => setJoinDialogOpen ( false ) }
456+ className = "px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200"
457+ disabled = { isJoining }
458+ >
459+ 取消
460+ </ button >
461+ < button
462+ onClick = { handleJoinGroupChat }
463+ disabled = { isJoining || ! roomIdInput . trim ( ) }
464+ className = { `px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 flex items-center
465+ ${ ( isJoining || ! roomIdInput . trim ( ) ) ? 'opacity-50 cursor-not-allowed' : '' } ` }
466+ >
467+ { isJoining ? (
468+ < >
469+ < svg className = "w-4 h-4 mr-1 animate-spin" fill = "none" viewBox = "0 0 24 24" >
470+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" > </ circle >
471+ < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </ path >
472+ </ svg >
473+ 加入中
474+ </ >
475+ ) : '加入' }
476+ </ button >
477+ </ div >
478+ </ Content >
479+ </ Portal >
480+ </ Root >
481+
327482 { /* 网络模式切换对话框 - 暂时保留但不显示 */ }
328483 < Root open = { networkModeDialogOpen } onOpenChange = { setNetworkModeDialogOpen } >
329484 < Portal >
0 commit comments