Skip to content

Commit 02443a6

Browse files
committed
feat: implement group chat joining functionality with input validation and URL processing
1 parent 5f7a940 commit 02443a6

File tree

6 files changed

+202
-53
lines changed

6 files changed

+202
-53
lines changed

src/page/chat/ChatPanel.tsx

Lines changed: 174 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import GroupUserList from '../group/GroupUserList.tsx'
99
import ChatInput from '../input/ChatInput.tsx'
1010
import MessageList from './MessageList.tsx'
1111
import { GroupChat } from '@/types/chat.ts'
12+
import { cleanRoomId } from '@/utils/roomUtils.ts'
1213

1314
// 动画常量
1415
const 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>

src/page/common/Avatar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { AvatarProps } from '@/types/chat.ts'
33

4-
const Avatar: React.FC<AvatarProps> = ({ src, alt, size = 'md', className = '' }) => {
4+
const Avatar: React.FC<AvatarProps> = ({ src, alt, size = 'md', className = '', isHost = false }) => {
55
const sizeClasses = {
66
sm: 'w-8 h-8',
77
md: 'w-10 h-10',
@@ -21,8 +21,10 @@ const Avatar: React.FC<AvatarProps> = ({ src, alt, size = 'md', className = '' }
2121
}
2222

2323
// 如果没有图片,显示首字母
24+
const bgColor = isHost ? 'bg-green-500' : 'bg-blue-500';
25+
2426
return (
25-
<div className={`${sizeClass} rounded-full bg-blue-500 flex items-center justify-center text-white ${className}`}>
27+
<div className={`${sizeClass} rounded-full ${bgColor} flex items-center justify-center text-white ${className}`}>
2628
{alt.charAt(0).toUpperCase()}
2729
</div>
2830
);

src/page/group/GroupChatHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const GroupChatHeader: React.FC = () => {
7878
return (
7979
<div className="p-4 flex items-center justify-between border-b border-gray-200">
8080
<div className="flex items-center">
81-
<Avatar alt={groupChat.name} size="md" />
81+
<Avatar alt={groupChat.name} size="md" isHost={groupChat.isHost} />
8282
<div className="ml-3">
8383
<h2 className="font-medium">{groupChat.name}</h2>
8484
<p className="text-xs text-gray-500">

src/page/group/GroupUserList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ const GroupUserList: React.FC = () => {
4646
<div className="space-y-3">
4747
{users.map(user => (
4848
<div key={user.id} className="flex items-center">
49-
<Avatar alt={user.name} size="sm" />
49+
<Avatar
50+
alt={user.name}
51+
size="sm"
52+
isHost={user.id === groupChat.roomId}
53+
/>
5054
<span className="ml-2 text-sm">
5155
{user.name} {user.id === userId && '(我)'}
5256
</span>
5357
{groupChat.isHost && user.id === groupChat.roomId && (
54-
<span className="ml-1 text-xs text-blue-500">(主持人)</span>
58+
<span className="ml-1 text-xs text-green-600">(主持人)</span>
5559
)}
5660
</div>
5761
))}

src/page/sidebar/Sidebar.tsx

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -208,29 +208,6 @@ const Sidebar: React.FC = () => {
208208
</TooltipContent>
209209
</TooltipRoot>
210210

211-
<TooltipRoot>
212-
<TooltipTrigger asChild>
213-
<button
214-
onClick={() => setNameDialogOpen(true)}
215-
disabled={isConnecting}
216-
className={`p-2 text-gray-500 hover:bg-gray-100 rounded-full
217-
${isConnecting ? 'opacity-50 cursor-not-allowed' : ''}`}
218-
>
219-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
220-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
221-
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
222-
/>
223-
</svg>
224-
</button>
225-
</TooltipTrigger>
226-
<TooltipContent
227-
className="bg-gray-800 text-white px-3 py-1.5 rounded text-sm animate-fadeIn z-50"
228-
sideOffset={5}
229-
>
230-
{userName ? "修改用户名" : "设置用户名"}
231-
</TooltipContent>
232-
</TooltipRoot>
233-
234211
<TooltipRoot>
235212
<TooltipTrigger asChild>
236213
<button
@@ -241,7 +218,7 @@ const Sidebar: React.FC = () => {
241218
>
242219
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
243220
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
244-
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"
221+
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
245222
/>
246223
</svg>
247224
</button>
@@ -281,25 +258,35 @@ const Sidebar: React.FC = () => {
281258

282259
{/* 用户信息 */}
283260
<div className="p-4 border-t border-gray-200">
284-
<div className="flex items-center">
261+
<button
262+
onClick={() => setNameDialogOpen(true)}
263+
disabled={isConnecting}
264+
className={`w-full flex items-center text-left transition-colors duration-200 rounded-lg p-2 -m-2
265+
${isConnecting ? 'cursor-not-allowed opacity-50' : 'hover:bg-gray-50 cursor-pointer'}`}
266+
>
285267
<div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white">
286268
{userName ? userName.charAt(0).toUpperCase() : '?'}
287269
</div>
288-
<div className="ml-3">
270+
<div className="ml-3 flex-1">
289271
<p className="font-medium">{userName || '未设置用户名'}</p>
290272
<p className="text-xs text-gray-500">
291273
{isConnecting ? (
292274
<span className="flex items-center">
293275
<svg className="w-3 h-3 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
294276
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
295-
<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>
277+
<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 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
296278
</svg>
297279
连接中...
298280
</span>
299-
) : '点击右上角图标修改用户名'}
281+
) : '点击修改用户名'}
300282
</p>
301283
</div>
302-
</div>
284+
{!isConnecting && (
285+
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
286+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
287+
</svg>
288+
)}
289+
</button>
303290
</div>
304291

305292
{/* 用户名输入对话框 */}

src/types/chat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface AvatarProps {
8383
alt: string
8484
size?: AvatarSize
8585
className?: string
86+
isHost?: boolean
8687
}
8788

8889
export interface BadgeProps {

0 commit comments

Comments
 (0)