Adds image sharing and message refactor to WRoom
This commit is contained in:
@@ -2,8 +2,8 @@ import { constructMetadata } from "@/lib/utils";
|
||||
import ChatRoom from "@/components/chat/chat-room";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "ChatRoom",
|
||||
description: "",
|
||||
title: "WRoom",
|
||||
description: "A temporary, peer-to-peer, and secure chat room",
|
||||
});
|
||||
|
||||
export default async function Page() {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// components/ChatRoom.tsx
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import randomName from "@scaleway/random-name";
|
||||
import { is } from "cheerio/lib/api/traversing";
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Image as ImageIcon,
|
||||
Menu,
|
||||
PanelLeftClose,
|
||||
PanelRightClose,
|
||||
@@ -30,7 +31,8 @@ import { Textarea } from "../ui/textarea";
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
text: string;
|
||||
text?: string;
|
||||
image?: string;
|
||||
isSelf: boolean;
|
||||
timestamp: string;
|
||||
username: string;
|
||||
@@ -58,14 +60,6 @@ const generateGradientClasses = (seed: string) => {
|
||||
"bg-gradient-to-br from-teal-400 to-green-500",
|
||||
"bg-gradient-to-br from-orange-400 to-yellow-500",
|
||||
"bg-gradient-to-br from-indigo-400 to-blue-500",
|
||||
"bg-gradient-to-br from-pink-400 to-red-500",
|
||||
"bg-gradient-to-br from-teal-400 to-green-500",
|
||||
"bg-gradient-to-br from-orange-400 to-yellow-500",
|
||||
"bg-gradient-to-br from-indigo-400 to-blue-500",
|
||||
"bg-gradient-to-br from-pink-400 to-red-500",
|
||||
"bg-gradient-to-br from-teal-400 to-green-500",
|
||||
"bg-gradient-to-br from-orange-400 to-yellow-500",
|
||||
"bg-gradient-to-br from-indigo-400 to-blue-500",
|
||||
];
|
||||
const hash = seed
|
||||
.split("")
|
||||
@@ -87,6 +81,7 @@ export default function ChatRoom() {
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [connectedCount, setConnectedCount] = useState(0);
|
||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(!isSm);
|
||||
const peerInstance = useRef<Peer | null>(null);
|
||||
@@ -95,6 +90,7 @@ export default function ChatRoom() {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const [isInvited, setIsInvited] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSm || isMobile) {
|
||||
@@ -105,11 +101,9 @@ export default function ChatRoom() {
|
||||
useEffect(() => {
|
||||
if ((remotePeerId || !!searchParams.get("room")) && !isConnected) {
|
||||
setIsInvited(true);
|
||||
// setRemotePeerId(peerId);
|
||||
}
|
||||
}, [remotePeerId, setRemotePeerId, searchParams.get("room"), isConnected]);
|
||||
}, [remotePeerId, searchParams, isConnected]);
|
||||
|
||||
// 在组件初始化时就设置用户名,避免未定义的用户名问题
|
||||
useEffect(() => {
|
||||
const newUsername = randomName("", ".");
|
||||
setUsername(newUsername);
|
||||
@@ -121,7 +115,6 @@ export default function ChatRoom() {
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// 清理函数,组件卸载时清理连接
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (peerInstance.current) {
|
||||
@@ -150,28 +143,28 @@ export default function ChatRoom() {
|
||||
if (peerInstance.current) return;
|
||||
|
||||
try {
|
||||
const peer = new Peer();
|
||||
const peer = new Peer({
|
||||
config: {
|
||||
iceServers: [
|
||||
{ urls: "stun:stun.l.google.com:19302" },
|
||||
{ urls: "stun:stun1.l.google.com:19302" },
|
||||
],
|
||||
},
|
||||
});
|
||||
peerInstance.current = peer;
|
||||
|
||||
peer.on("open", (id) => {
|
||||
setPeerId(id);
|
||||
// 初始化自己为用户列表的第一个
|
||||
setUsers((prev) => {
|
||||
const newUser = { peerId: id, username };
|
||||
// 确保不重复添加
|
||||
if (!prev.some((u) => u.peerId === id)) {
|
||||
return [...prev, newUser];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
setConnectedCount(1); // 至少包括自己
|
||||
|
||||
if (isInvited && remotePeerId && !isConnected) {
|
||||
// connectToPeer(); // 被邀请者自动连接, BUG: 自动连接可能会导致用户列表无法刷新
|
||||
}
|
||||
setConnectedCount(1);
|
||||
});
|
||||
|
||||
// 只有非被邀请用户(中继服务器)监听新连接
|
||||
if (!isInvited) {
|
||||
peer.on("connection", (conn) => {
|
||||
connections.current.push(conn);
|
||||
@@ -193,7 +186,7 @@ export default function ChatRoom() {
|
||||
console.error("Failed to initialize peer:", err);
|
||||
toast.error("Failed to initialize peer connection");
|
||||
}
|
||||
}, [username, isInvited, remotePeerId, isConnected]);
|
||||
}, [username, isInvited]);
|
||||
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
@@ -204,106 +197,108 @@ export default function ChatRoom() {
|
||||
const handleConnection = (conn: any) => {
|
||||
conn.on("open", () => {
|
||||
setIsConnected(true);
|
||||
|
||||
// 更新连接计数
|
||||
setConnectedCount((prev) => {
|
||||
const newCount = prev + 1;
|
||||
broadcastCount(newCount);
|
||||
return newCount;
|
||||
});
|
||||
|
||||
// 发送当前用户列表给新连接的客户端
|
||||
setTimeout(() => {
|
||||
const userList = JSON.stringify(users);
|
||||
conn.send(`USERLIST:${userList}`);
|
||||
}, 100); // 延迟发送,确保状态已更新
|
||||
conn.send({ type: "USERLIST", data: userList });
|
||||
}, 100);
|
||||
});
|
||||
|
||||
conn.on("data", (data: string) => {
|
||||
if (data.startsWith("JOIN:")) {
|
||||
const [_, joiningUsername, joiningPeerId] = data.split(":");
|
||||
const joinMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${joiningUsername}] entered the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, joinMessage]);
|
||||
|
||||
// 更新用户列表,确保不重复添加
|
||||
setUsers((prev) => {
|
||||
if (!prev.some((u) => u.peerId === joiningPeerId)) {
|
||||
const updatedUsers = [
|
||||
...prev,
|
||||
{ peerId: joiningPeerId, username: joiningUsername },
|
||||
];
|
||||
setTimeout(() => {
|
||||
broadcastUserList(updatedUsers);
|
||||
broadcastCount(updatedUsers.length);
|
||||
}, 100);
|
||||
return updatedUsers;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
broadcastMessage(joinMessage, conn);
|
||||
} else if (data.startsWith("COUNT:")) {
|
||||
const count = parseInt(data.split("COUNT:")[1], 10);
|
||||
setConnectedCount(count);
|
||||
} else if (data.startsWith("USERLIST:")) {
|
||||
try {
|
||||
const userList = JSON.parse(data.split("USERLIST:")[1]);
|
||||
conn.on("data", (data: any) => {
|
||||
if (typeof data === "object" && data.type) {
|
||||
if (data.type === "JOIN") {
|
||||
const { username: joiningUsername, peerId: joiningPeerId } =
|
||||
data.data;
|
||||
const joinMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${joiningUsername}] entered the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, joinMessage]);
|
||||
setUsers((prev) => {
|
||||
const mergedUsers = [...userList];
|
||||
if (!mergedUsers.some((u) => u.peerId === peerId)) {
|
||||
mergedUsers.push({ peerId, username });
|
||||
if (!prev.some((u) => u.peerId === joiningPeerId)) {
|
||||
const updatedUsers = [
|
||||
...prev,
|
||||
{ peerId: joiningPeerId, username: joiningUsername },
|
||||
];
|
||||
setTimeout(() => {
|
||||
broadcastUserList(updatedUsers);
|
||||
broadcastCount(updatedUsers.length);
|
||||
}, 100);
|
||||
return updatedUsers;
|
||||
}
|
||||
// 去除重复项
|
||||
return mergedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((u) => u.peerId === user.peerId),
|
||||
);
|
||||
return prev;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error parsing user list:", err);
|
||||
broadcastMessage(joinMessage, conn);
|
||||
} else if (data.type === "COUNT") {
|
||||
setConnectedCount(data.data);
|
||||
} else if (data.type === "USERLIST") {
|
||||
try {
|
||||
const userList = JSON.parse(data.data);
|
||||
setUsers((prev) => {
|
||||
const mergedUsers = [...userList];
|
||||
if (!mergedUsers.some((u) => u.peerId === peerId)) {
|
||||
mergedUsers.push({ peerId, username });
|
||||
}
|
||||
return mergedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((u) => u.peerId === user.peerId),
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error parsing user list:", err);
|
||||
}
|
||||
} else if (data.type === "LEAVE") {
|
||||
const { username: leavingUsername, peerId: leavingPeerId } =
|
||||
data.data;
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${leavingUsername}] left the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
broadcastMessage(leaveMessage, conn);
|
||||
} else if (data.type === "IMAGE") {
|
||||
const { username: senderUsername, image } = data.data;
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
image,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: senderUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
broadcastMessage(newMessage, conn);
|
||||
} else if (data.type === "TEXT") {
|
||||
const { username: senderUsername, text } = data.data;
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: senderUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
broadcastMessage(newMessage, conn);
|
||||
}
|
||||
} else if (data.startsWith("LEAVE:")) {
|
||||
const [_, leavingUsername, leavingPeerId] = data.split(":");
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${leavingUsername}] left the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
broadcastMessage(leaveMessage, conn);
|
||||
} else {
|
||||
const [receivedUsername, ...messageParts] = data.split(": ");
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: messageParts.join(": "),
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: receivedUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
broadcastMessage(newMessage, conn);
|
||||
}
|
||||
});
|
||||
|
||||
conn.on("close", () => {
|
||||
connections.current = connections.current.filter((c) => c !== conn);
|
||||
|
||||
// 找到断开连接的用户
|
||||
const disconnectedUser = users.find((user) => user.peerId === conn.peer);
|
||||
|
||||
if (disconnectedUser) {
|
||||
// 创建离开的系统消息
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${disconnectedUser.username}] left the room`,
|
||||
@@ -313,8 +308,6 @@ export default function ChatRoom() {
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
|
||||
// 更新用户列表
|
||||
setUsers((prev) => {
|
||||
const updatedUsers = prev.filter((user) => user.peerId !== conn.peer);
|
||||
setTimeout(() => {
|
||||
@@ -324,8 +317,6 @@ export default function ChatRoom() {
|
||||
return updatedUsers;
|
||||
});
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
if (!connRef.current && connections.current.length === 0) {
|
||||
setIsConnected(false);
|
||||
}
|
||||
@@ -333,11 +324,20 @@ export default function ChatRoom() {
|
||||
};
|
||||
|
||||
const broadcastMessage = (message: Message, senderConn: any) => {
|
||||
// 确保只广播到相关的连接
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn !== senderConn && conn.open) {
|
||||
try {
|
||||
conn.send(`${message.username}: ${message.text}`);
|
||||
if (message.image) {
|
||||
conn.send({
|
||||
type: "IMAGE",
|
||||
data: { username: message.username, image: message.image },
|
||||
});
|
||||
} else if (message.text) {
|
||||
conn.send({
|
||||
type: "TEXT",
|
||||
data: { username: message.username, text: message.text },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error broadcasting message:", err);
|
||||
}
|
||||
@@ -349,7 +349,7 @@ export default function ChatRoom() {
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn.open) {
|
||||
try {
|
||||
conn.send(`COUNT:${count}`);
|
||||
conn.send({ type: "COUNT", data: count });
|
||||
} catch (err) {
|
||||
console.error("Error broadcasting count:", err);
|
||||
}
|
||||
@@ -363,7 +363,7 @@ export default function ChatRoom() {
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn.open) {
|
||||
try {
|
||||
conn.send(`USERLIST:${userList}`);
|
||||
conn.send({ type: "USERLIST", data: userList });
|
||||
} catch (err) {
|
||||
console.error("Error broadcasting user list:", err);
|
||||
}
|
||||
@@ -383,88 +383,94 @@ export default function ChatRoom() {
|
||||
connRef.current = conn;
|
||||
|
||||
conn.on("open", () => {
|
||||
conn.send(`JOIN:${username}:${peerId}`);
|
||||
conn.send({ type: "JOIN", data: { username, peerId } });
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
conn.on("data", (data: string) => {
|
||||
if (data.startsWith("JOIN:")) {
|
||||
const [_, joiningUsername, joiningPeerId] = data.split(":");
|
||||
const joinMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${joiningUsername}] entered the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, joinMessage]);
|
||||
|
||||
// 更新用户列表,确保不重复添加
|
||||
setUsers((prev) => {
|
||||
if (!prev.some((u) => u.peerId === joiningPeerId)) {
|
||||
return [
|
||||
...prev,
|
||||
{ peerId: joiningPeerId, username: joiningUsername },
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
} else if (data.startsWith("COUNT:")) {
|
||||
const count = parseInt(data.split("COUNT:")[1], 10);
|
||||
setConnectedCount(count);
|
||||
} else if (data.startsWith("USERLIST:")) {
|
||||
try {
|
||||
const userList = JSON.parse(data.split("USERLIST:")[1]);
|
||||
conn.on("data", (data: any) => {
|
||||
if (typeof data === "object" && data.type) {
|
||||
if (data.type === "JOIN") {
|
||||
const { username: joiningUsername, peerId: joiningPeerId } =
|
||||
data.data;
|
||||
const joinMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${joiningUsername}] entered the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, joinMessage]);
|
||||
setUsers((prev) => {
|
||||
const mergedUsers = [...userList];
|
||||
if (!mergedUsers.some((u) => u.peerId === peerId)) {
|
||||
mergedUsers.push({ peerId, username });
|
||||
if (!prev.some((u) => u.peerId === joiningPeerId)) {
|
||||
return [
|
||||
...prev,
|
||||
{ peerId: joiningPeerId, username: joiningUsername },
|
||||
];
|
||||
}
|
||||
// 去除重复项
|
||||
return mergedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((u) => u.peerId === user.peerId),
|
||||
);
|
||||
return prev;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error parsing user list:", err);
|
||||
} else if (data.type === "COUNT") {
|
||||
setConnectedCount(data.data);
|
||||
} else if (data.type === "USERLIST") {
|
||||
try {
|
||||
const userList = JSON.parse(data.data);
|
||||
setUsers((prev) => {
|
||||
const mergedUsers = [...userList];
|
||||
if (!mergedUsers.some((u) => u.peerId === peerId)) {
|
||||
mergedUsers.push({ peerId, username });
|
||||
}
|
||||
return mergedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((u) => u.peerId === user.peerId),
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error parsing user list:", err);
|
||||
}
|
||||
} else if (data.type === "LEAVE") {
|
||||
const { username: leavingUsername, peerId: leavingPeerId } =
|
||||
data.data;
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${leavingUsername}] left the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
} else if (data.type === "IMAGE") {
|
||||
const { username: senderUsername, image } = data.data;
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
image,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: senderUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
} else if (data.type === "TEXT") {
|
||||
const { username: senderUsername, text } = data.data;
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: senderUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
}
|
||||
} else if (data.startsWith("LEAVE:")) {
|
||||
const [_, leavingUsername, leavingPeerId] = data.split(":");
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${leavingUsername}] left the room`,
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: "System",
|
||||
isSystem: true,
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
} else {
|
||||
const [receivedUsername, ...messageParts] = data.split(": ");
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: messageParts.join(": "),
|
||||
isSelf: false,
|
||||
timestamp: formatTime(new Date()),
|
||||
username: receivedUsername,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
}
|
||||
});
|
||||
|
||||
conn.on("close", () => {
|
||||
setIsConnected(false);
|
||||
connRef.current = null;
|
||||
|
||||
// 找到断开连接的用户
|
||||
const disconnectedUser = users.find(
|
||||
(user) => user.peerId === remotePeerId,
|
||||
);
|
||||
|
||||
if (disconnectedUser) {
|
||||
// 创建离开的系统消息
|
||||
const leaveMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: `[${disconnectedUser.username}] left the room`,
|
||||
@@ -475,8 +481,6 @@ export default function ChatRoom() {
|
||||
};
|
||||
setMessages((prev) => [...prev, leaveMessage]);
|
||||
}
|
||||
|
||||
// 更新用户列表
|
||||
setUsers((prev) => {
|
||||
const updatedUsers = prev.filter(
|
||||
(user) => user.peerId !== remotePeerId,
|
||||
@@ -500,11 +504,67 @@ export default function ChatRoom() {
|
||||
}
|
||||
}, [remotePeerId, username, peerId, users]);
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error("Image size should not exceed 5MB");
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setSelectedImage(file);
|
||||
sendImage(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const sendImage = (base64Image: string) => {
|
||||
if (!base64Image || isSending) return;
|
||||
|
||||
setIsSending(true);
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
image: base64Image,
|
||||
isSelf: true,
|
||||
timestamp: formatTime(new Date()),
|
||||
username,
|
||||
};
|
||||
setMessages((prev) => [...prev, newMessage]);
|
||||
|
||||
try {
|
||||
if (connRef.current && connRef.current.open) {
|
||||
connRef.current.send({
|
||||
type: "IMAGE",
|
||||
data: { username, image: base64Image },
|
||||
});
|
||||
} else if (!isInvited && connections.current.length > 0) {
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn.open) {
|
||||
conn.send({
|
||||
type: "IMAGE",
|
||||
data: { username, image: base64Image },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error sending image:", err);
|
||||
toast.error("Failed to send image");
|
||||
}
|
||||
|
||||
setSelectedImage(null);
|
||||
setIsSending(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessage = useCallback(() => {
|
||||
if (!message || isSending) return;
|
||||
|
||||
setIsSending(true);
|
||||
const formattedMessage = `${username}: ${message}`;
|
||||
const newMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
text: message,
|
||||
@@ -516,12 +576,14 @@ export default function ChatRoom() {
|
||||
|
||||
try {
|
||||
if (connRef.current && connRef.current.open) {
|
||||
connRef.current.send(formattedMessage);
|
||||
connRef.current.send({
|
||||
type: "TEXT",
|
||||
data: { username, text: message },
|
||||
});
|
||||
} else if (!isInvited && connections.current.length > 0) {
|
||||
// 只广播到当前房间的连接
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn.open) {
|
||||
conn.send(formattedMessage);
|
||||
conn.send({ type: "TEXT", data: { username, text: message } });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -569,12 +631,11 @@ export default function ChatRoom() {
|
||||
|
||||
const createNewRoom = () => {
|
||||
if (peerInstance.current) {
|
||||
// 通知其他用户我要离开
|
||||
if (username && peerId) {
|
||||
connections.current.forEach((conn) => {
|
||||
if (conn.open) {
|
||||
try {
|
||||
conn.send(`LEAVE:${username}:${peerId}`);
|
||||
conn.send({ type: "LEAVE", data: { username, peerId } });
|
||||
} catch (err) {
|
||||
console.error("Error sending leave notification:", err);
|
||||
}
|
||||
@@ -582,7 +643,6 @@ export default function ChatRoom() {
|
||||
});
|
||||
}
|
||||
|
||||
// 清理当前连接
|
||||
peerInstance.current.destroy();
|
||||
peerInstance.current = null;
|
||||
connections.current.forEach((conn) => {
|
||||
@@ -594,7 +654,6 @@ export default function ChatRoom() {
|
||||
}
|
||||
connRef.current = null;
|
||||
|
||||
// 重置状态
|
||||
setPeerId("");
|
||||
setRemotePeerId("");
|
||||
setMessages([]);
|
||||
@@ -602,7 +661,6 @@ export default function ChatRoom() {
|
||||
setIsConnected(false);
|
||||
setConnectedCount(0);
|
||||
|
||||
// 重新初始化
|
||||
setTimeout(() => {
|
||||
initializePeer();
|
||||
}, 500);
|
||||
@@ -611,7 +669,6 @@ export default function ChatRoom() {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gradient-to-br from-blue-50 to-neutral-100 dark:from-neutral-900 dark:to-neutral-800">
|
||||
{/* 侧边栏 */}
|
||||
{(isMobile && isSidebarOpen) || (!isMobile && isSidebarOpen) ? (
|
||||
<div
|
||||
className={`${
|
||||
@@ -627,7 +684,7 @@ export default function ChatRoom() {
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="p-0 text-neutral-600 hover:text-blue-500 dark:text-neutral-400 dark:hover:text-blue-400"
|
||||
className="h-5 p-0 text-neutral-600 hover:text-blue-500 dark:text-neutral-400 dark:hover:text-blue-400"
|
||||
>
|
||||
{!isSidebarOpen ? (
|
||||
<PanelRightClose size={20} />
|
||||
@@ -659,53 +716,63 @@ export default function ChatRoom() {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{!isInvited && (
|
||||
<Button
|
||||
onClick={createNewRoom}
|
||||
variant={"outline"}
|
||||
className="mt-auto flex items-center justify-center gap-1"
|
||||
>
|
||||
<Plus size={20} />
|
||||
New Room
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={createNewRoom}
|
||||
variant={"outline"}
|
||||
className="mt-auto flex items-center justify-center gap-1"
|
||||
>
|
||||
<Plus size={20} />
|
||||
New Room
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 聊天框 */}
|
||||
<div
|
||||
className={`flex flex-1 flex-col bg-white px-4 pb-1 pt-3 transition-all duration-300 dark:bg-neutral-800 ${
|
||||
className={`grids grids-dark flex flex-1 flex-col bg-white px-4 pb-1 pt-3 transition-all duration-300 dark:bg-neutral-800 ${
|
||||
isMobile || !isSidebarOpen ? "w-full" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mb-4 flex items-center justify-between gap-4">
|
||||
<div className="mr-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="p-0 text-neutral-600 hover:text-blue-500 dark:text-neutral-400 dark:hover:text-blue-400"
|
||||
className="h-5 p-0 text-neutral-600 hover:text-blue-500 dark:text-neutral-400 dark:hover:text-blue-400"
|
||||
>
|
||||
{!isSidebarOpen && <PanelRightClose size={20} />}
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold text-neutral-800 dark:text-neutral-100">
|
||||
Chat Room
|
||||
</h1>
|
||||
<Link
|
||||
href={"/chat"}
|
||||
className="text-2xl font-bold text-neutral-800 dark:text-neutral-100"
|
||||
style={{ fontFamily: "Bahamas Bold" }}
|
||||
>
|
||||
WRoom
|
||||
</Link>
|
||||
<Badge>Beta</Badge>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Link
|
||||
className="text-sm hover:underline"
|
||||
href={"/dashboard"}
|
||||
target="_blank"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
className="text-sm hover:underline"
|
||||
href={"/docs/wroom"}
|
||||
target="_blank"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="flex items-center gap-1 text-xs text-green-300 dark:text-green-700">
|
||||
<Icons.users className="size-3" /> Online: {connectedCount}
|
||||
</Badge>
|
||||
|
||||
{/* <span
|
||||
className={`rounded-full px-2 py-1 text-xs ${
|
||||
isConnected
|
||||
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
||||
}`}
|
||||
>
|
||||
{isConnected ? "Connected" : "Disconnected"}
|
||||
</span> */}
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
@@ -713,7 +780,7 @@ export default function ChatRoom() {
|
||||
<div className="mb-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Your ID:
|
||||
Your ID:
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -745,13 +812,12 @@ export default function ChatRoom() {
|
||||
Room ID:
|
||||
</span>
|
||||
{!isInvited && isConnected ? (
|
||||
// <Badge>Room owner</Badge>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Your are the room owner"
|
||||
placeholder="You are the room owner"
|
||||
readOnly
|
||||
disabled
|
||||
className="flex-1 rounded border focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder-neutral-400"
|
||||
className="flex-1 rounded border bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
@@ -761,14 +827,13 @@ export default function ChatRoom() {
|
||||
placeholder="Enter a room id"
|
||||
readOnly={isConnected}
|
||||
disabled={isConnected}
|
||||
className="flex-1 rounded border focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder-neutral-400"
|
||||
className="flex-1 rounded border bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder-neutral-400"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={connectToPeer}
|
||||
disabled={!remotePeerId || isConnected}
|
||||
disabled={!peerId || !remotePeerId || isConnected}
|
||||
size={"sm"}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded bg-blue-500 text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-neutral-400 dark:bg-blue-600 dark:hover:bg-blue-700",
|
||||
@@ -782,7 +847,7 @@ export default function ChatRoom() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full min-h-[100px] overflow-y-auto rounded-md rounded-b-none border border-neutral-200 p-4 dark:border-neutral-600 dark:bg-neutral-700">
|
||||
<div className="h-full min-h-[100px] overflow-y-auto rounded-md rounded-b-none border border-neutral-200 bg-white p-4 dark:border-neutral-600 dark:bg-neutral-700">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
@@ -820,7 +885,15 @@ export default function ChatRoom() {
|
||||
: "bg-neutral-200 text-neutral-800 dark:bg-neutral-600 dark:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{msg.text}</p>
|
||||
{msg.text && <p className="text-sm">{msg.text}</p>}
|
||||
{msg.image && (
|
||||
<img
|
||||
src={msg.image}
|
||||
alt="Sent image"
|
||||
className="max-w-full rounded-md"
|
||||
style={{ maxHeight: "200px" }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"mt-1 block text-xs opacity-70",
|
||||
@@ -855,21 +928,42 @@ export default function ChatRoom() {
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder={`Hi ${username || "Loading..."}, send a message to start...`}
|
||||
className="min-h-20 flex-1 rounded-md rounded-t-none border border-t-0 bg-neutral-50 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 dark:border-neutral-600 dark:bg-neutral-600 dark:text-neutral-200 dark:placeholder-neutral-400"
|
||||
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
|
||||
onKeyPress={(e) =>
|
||||
e.key === "Enter" && !e.shiftKey && sendMessage()
|
||||
}
|
||||
disabled={!isConnected}
|
||||
/>
|
||||
<Button
|
||||
onClick={sendMessage}
|
||||
disabled={!isConnected || !message || isSending}
|
||||
className="absolute bottom-2 right-2"
|
||||
size={"sm"}
|
||||
>
|
||||
{isSending ? (
|
||||
<span className="animate-spin">⌛</span>
|
||||
) : (
|
||||
<Icons.send className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
<div className="absolute bottom-2 right-2 flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={!isConnected || isSending}
|
||||
title="Send Image"
|
||||
>
|
||||
<ImageIcon size={16} />
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
onClick={sendMessage}
|
||||
disabled={
|
||||
!isConnected || (!message && !selectedImage) || isSending
|
||||
}
|
||||
size={"sm"}
|
||||
>
|
||||
{isSending ? (
|
||||
<span className="animate-spin">⌛</span>
|
||||
) : (
|
||||
<Icons.send className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="mt-2 py-2 text-center text-sm font-semibold text-neutral-600 dark:text-neutral-300">
|
||||
|
||||
@@ -12,6 +12,7 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
{ href: "/dashboard/urls", icon: "link", title: "Short Urls" },
|
||||
{ href: "/emails", icon: "mail", title: "Emails" },
|
||||
{ href: "/dashboard/records", icon: "globeLock", title: "DNS Records" },
|
||||
{ href: "/chat", icon: "messages", title: "WRoom" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -59,18 +60,18 @@ export const sidebarLinks: SidebarNavItem[] = [
|
||||
title: "Users",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/records",
|
||||
icon: "globe",
|
||||
title: "Records",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/urls",
|
||||
icon: "link",
|
||||
title: "URLs",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
{
|
||||
href: "/admin/records",
|
||||
icon: "globe",
|
||||
title: "Records",
|
||||
authorizeOnly: UserRole.ADMIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: "/docs/dns-records",
|
||||
icon: "page",
|
||||
},
|
||||
{
|
||||
title: "WRoom",
|
||||
href: "/docs/wroom",
|
||||
icon: "page",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
196
content/docs/wroom.mdx
Normal file
196
content/docs/wroom.mdx
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
title: WRoom
|
||||
description: A temporary, peer-to-peer, and secure chat room
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
The **WRoom** is a decentralized, browser-based application built using **PeerJS**, enabling real-time communication between users without relying on a central server. Leveraging WebRTC technology, this chat room supports both text and image messaging, making it a versatile tool for peer-to-peer interaction. The application features a modern, responsive UI built with **React** and **Next.js**, offering a seamless experience across desktop and mobile devices.
|
||||
|
||||
### Key Features
|
||||
- **Decentralized Communication**: Uses PeerJS for direct peer-to-peer connections, eliminating the need for a central server.
|
||||
- **Text Messaging**: Send and receive real-time text messages with timestamps and usernames.
|
||||
- **Image Messaging**: Share images (up to 5MB) with other users in the chat room, displayed inline with messages.
|
||||
- **User Management**: Displays a list of connected users with unique avatars and usernames.
|
||||
- **Room Creation and Sharing**: Create new rooms or join existing ones using a unique peer ID, with easy sharing via a generated URL.
|
||||
- **Responsive Design**: Adapts to various screen sizes, with a collapsible sidebar for mobile users.
|
||||
- **Connection Status**: Real-time indicators for online users and connection state.
|
||||
- **Dark Mode**: Toggle between light and dark themes for user preference.
|
||||
|
||||
### Technical Highlights
|
||||
- Built with **Next.js** for server-side rendering and client-side interactivity.
|
||||
- Utilizes **PeerJS** with STUN servers for reliable WebRTC connections.
|
||||
- Messages are transmitted as structured objects to ensure data integrity, especially for Base64-encoded images.
|
||||
- Random usernames are generated using the `@scaleway/random-name` library.
|
||||
- Styled with **Tailwind CSS** and custom gradient-based avatars.
|
||||
|
||||
## Usage Guide
|
||||
|
||||
This section provides step-by-step instructions on how to use the WRoom effectively.
|
||||
|
||||
### Prerequisites
|
||||
- A modern web browser (e.g., Chrome, Firefox, Edge) with WebRTC support.
|
||||
- An internet connection (required for initial peer connection via STUN servers).
|
||||
- No additional software installation is needed—just open the application in your browser.
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Access the Chat Room**
|
||||
- Open [https://wr.do/chat](https://wr.do/chat) in your browser.
|
||||
- The chat room will automatically initialize and assign you a unique **Peer ID**.
|
||||
|
||||
2. **Create a New Room**
|
||||
- Upon loading, you are the "owner" of a new chat room by default.
|
||||
- Your **Peer ID** (e.g., `abc123-xyz789`) is displayed under "Your ID."
|
||||
- Share this ID with others to invite them to your room.
|
||||
|
||||
3. **Join an Existing Room**
|
||||
- If you have a room ID from another user, enter it in the "Room ID" field.
|
||||
- Click the **Connect** button to join the room.
|
||||
- Once connected, the button will turn green and display "Connected."
|
||||
|
||||
4. **Share Your Room**
|
||||
- Click the **Share Room** button next to "Your ID" to copy a URL (e.g., `https://wr.do/chat?room=abc123-xyz789`).
|
||||
- Send this URL to others via email, messaging apps, or any preferred method.
|
||||
- Recipients can open the URL to join your room directly.
|
||||
|
||||
### Sending Messages
|
||||
|
||||
1. **Text Messages**
|
||||
- Type your message in the text area at the bottom of the chat window.
|
||||
- Press **Enter** (without Shift) or click the **Send** button (paper plane icon) to send.
|
||||
- Your message will appear on the right side with your username and timestamp, while others' messages appear on the left.
|
||||
|
||||
2. **Image Messages**
|
||||
- Click the **Image** icon (next to the Send button) to open a file picker.
|
||||
- Select an image file from your device (max size: 5MB, supported formats: PNG, JPG, etc.).
|
||||
- The image will be sent automatically and displayed in the chat with your username and timestamp.
|
||||
- *Note*: If the image exceeds 5MB, an error toast will appear.
|
||||
|
||||
### Managing the Chat Room
|
||||
|
||||
- **View Users**
|
||||
- Toggle the sidebar (via the menu icon) to see a list of connected users.
|
||||
- The room owner is marked with an "Owner" badge.
|
||||
- Each user has a unique gradient avatar based on their username.
|
||||
|
||||
- **Create a New Room**
|
||||
- If you’re the owner, click **New Room** in the sidebar to reset the current room and generate a new Peer ID.
|
||||
- This disconnects all users and starts a fresh session.
|
||||
|
||||
- **Disconnect**
|
||||
- Simply close the browser tab to leave the room. If you’re the owner, the room persists as long as other peers remain connected.
|
||||
|
||||
### UI Customization
|
||||
- **Dark Mode**: Use the toggle in the top-right corner to switch between light and dark themes.
|
||||
- **Sidebar**: On mobile devices, the sidebar is hidden by default; tap the menu icon to show it.
|
||||
|
||||
### Troubleshooting
|
||||
- **Connection Issues**: If "Disconnected" appears, check your internet connection or try refreshing the page.
|
||||
- **Image Not Displaying**: Ensure the image is under 5MB and in a supported format. If the issue persists, verify the recipient’s browser console for errors.
|
||||
- **Peer ID Not Working**: Confirm the ID is correct and the room owner is still online.
|
||||
|
||||
## Notes
|
||||
- **Limitations**: The chat room relies on WebRTC, so firewall or network restrictions may affect connectivity. Images larger than 5MB are blocked to prevent performance issues.
|
||||
- **Privacy**: No messages are stored server-side; all communication is peer-to-peer. However, use caution when sharing sensitive information.
|
||||
- **Future Enhancements**: Potential features include file compression, multi-file support, or end-to-end encryption.
|
||||
|
||||
This WRoom offers a lightweight, user-friendly solution for real-time communication. Enjoy chatting and sharing with your peers at [https://wr.do/chat](https://wr.do/chat)!
|
||||
|
||||
|
||||
Chinese (Simplified):
|
||||
|
||||
## 介绍
|
||||
|
||||
**WRoom 聊天室** 是一款基于浏览器的去中心化应用程序,利用 **PeerJS** 实现用户之间的实时通信,无需依赖中央服务器。通过 WebRTC 技术,该聊天室支持文本和图片消息,使其成为一个多功能的点对点交互工具。应用采用 **React** 和 **Next.js** 构建,拥有现代化的响应式用户界面,适用于桌面和移动设备。
|
||||
|
||||
### 主要功能
|
||||
- **去中心化通信**:通过 PeerJS 实现直接的点对点连接,无需中央服务器。
|
||||
- **文本消息**:实时发送和接收带有时间戳和用户名的文本消息。
|
||||
- **图片消息**:与聊天室中的其他用户分享图片(最大 5MB),图片将内嵌显示在消息中。
|
||||
- **用户管理**:显示连接用户的列表,每个用户拥有独特的头像和用户名。
|
||||
- **房间创建与分享**:创建新房间或加入现有房间,使用唯一的 Peer ID,并通过生成的 URL 轻松分享。
|
||||
- **响应式设计**:适配不同屏幕尺寸,移动端用户可折叠侧边栏。
|
||||
- **连接状态**:实时显示在线用户数和连接状态。
|
||||
- **暗色模式**:可在浅色和深色主题之间切换,满足用户偏好。
|
||||
|
||||
### 技术亮点
|
||||
- 使用 **Next.js** 实现服务端渲染和客户端交互。
|
||||
- 利用 **PeerJS** 和 STUN 服务器确保 WebRTC 连接的可靠性。
|
||||
- 消息以结构化对象形式传输,确保数据完整性,特别是 Base64 编码的图片。
|
||||
- 使用 `@scaleway/random-name` 库生成随机用户名。
|
||||
- 使用 **Tailwind CSS** 进行样式设计,搭配基于梯度的自定义头像。
|
||||
|
||||
## 使用指南
|
||||
|
||||
本节提供逐步说明,帮助您有效使用 P2P 聊天室。
|
||||
|
||||
### 前提条件
|
||||
- 一个支持 WebRTC 的现代浏览器(如 Chrome、Firefox、Edge)。
|
||||
- 互联网连接(通过 STUN 服务器建立初始 Peer 连接时需要)。
|
||||
- 无需安装额外软件,直接在浏览器中打开应用即可。
|
||||
|
||||
### 开始使用
|
||||
|
||||
1. **访问聊天室**
|
||||
- 在浏览器中打开 [https://wr.do/chat](https://wr.do/chat)。
|
||||
- 聊天室将自动初始化并为您分配一个唯一的 **Peer ID**。
|
||||
|
||||
2. **创建新房间**
|
||||
- 加载应用后,您默认成为新聊天室的“拥有者”。
|
||||
- 您的 **Peer ID**(例如 `abc123-xyz789`)会显示在“Your ID”字段中。
|
||||
- 将此 ID 分享给他人,邀请他们加入您的房间。
|
||||
|
||||
3. **加入现有房间**
|
||||
- 如果您有其他用户的房间 ID,请在“Room ID”字段中输入。
|
||||
- 点击 **Connect(连接)** 按钮加入房间。
|
||||
- 连接成功后,按钮将变为绿色并显示“Connected(已连接)”。
|
||||
|
||||
4. **分享您的房间**
|
||||
- 点击“Your ID”旁边的 **Share Room(分享房间)** 按钮,复制包含您 Peer ID 的 URL(例如 `https://wr.do/chat?room=abc123-xyz789`)。
|
||||
- 通过电子邮件、消息应用或其他方式将此 URL 发送给他人。
|
||||
- 接收者可直接打开 URL 加入您的房间。
|
||||
|
||||
### 发送消息
|
||||
|
||||
1. **文本消息**
|
||||
- 在聊天窗口底部的文本区域输入消息。
|
||||
- 按 **Enter** 键(不按 Shift)或点击 **Send(发送)** 按钮(纸飞机图标)发送。
|
||||
- 您的消息将显示在右侧,带有您的用户名和时间戳;他人的消息显示在左侧。
|
||||
|
||||
2. **图片消息**
|
||||
- 点击 **Image(图片)** 图标(位于发送按钮旁)打开文件选择器。
|
||||
- 从设备中选择图片文件(最大 5MB,支持 PNG、JPG 等格式)。
|
||||
- 图片将自动发送并在聊天中显示,带有您的用户名和时间戳。
|
||||
- *注意*:如果图片超过 5MB,会显示错误提示。
|
||||
|
||||
### 管理聊天室
|
||||
|
||||
- **查看用户**
|
||||
- 通过菜单图标切换侧边栏,查看连接用户列表。
|
||||
- 房间拥有者标有“Owner(拥有者)”徽章。
|
||||
- 每个用户拥有基于用户名的独特梯度头像。
|
||||
|
||||
- **创建新房间**
|
||||
- 如果您是拥有者,点击侧边栏中的 **New Room(新房间)** 重置当前房间并生成新的 Peer ID。
|
||||
- 这将断开所有用户连接并开始一个新会话。
|
||||
|
||||
- **断开连接**
|
||||
- 关闭浏览器标签即可离开房间。如果您是拥有者,只要其他 Peer 仍在线,房间会继续存在。
|
||||
|
||||
### 界面自定义
|
||||
- **暗色模式**:使用右上角的切换按钮在浅色和深色主题间切换。
|
||||
- **侧边栏**:在移动设备上,侧边栏默认隐藏,点击菜单图标可显示。
|
||||
|
||||
### 故障排除
|
||||
- **连接问题**:如果显示“Disconnected(已断开)”,请检查网络连接或刷新页面。
|
||||
- **图片未显示**:确保图片小于 5MB 且格式支持。如问题持续,请检查接收方浏览器控制台是否有错误。
|
||||
- **Peer ID 无效**:确认 ID 正确且房间拥有者仍在线。
|
||||
|
||||
|
||||
## 注意事项
|
||||
- **限制**:聊天室依赖 WebRTC,防火墙或网络限制可能影响连接。图片大小超过 5MB 将被阻止以避免性能问题。
|
||||
- **隐私**:消息不存储在服务器端,所有通信均为点对点。但分享敏感信息时仍需谨慎。
|
||||
- **未来改进**:可能添加的功能包括文件压缩、多文件支持或端到端加密。
|
||||
|
||||
**WRoom** 提供了一个轻量级、用户友好的实时通信解决方案。立即访问 [https://wr.do/chat](https://wr.do/chat),享受与您的朋友聊天和分享吧!
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user