群聊天:群消息
添加 src/components/group.jsx:
import React from "react";
import "./Contact.css";
const Group = (props) => {
return (
<div className="contact" onClick={props.onClick}>
<div className="name truncate">{props.name}</div>
<div className="last-message truncate">
{props.message || "[no messages]"}
</div>
</div>
);
};
export default Group;
它用于在群列表中展示各个群组名。
群聊天抬头需隐去“+”按钮,src/components/title-bar.jsx:
@@ -5,14 +5,18 @@ const TitleBar = (props) => {
return (
<div className="title">
<span></span>
- <span>{props.username}</span>
- <button
- className="create-btn"
- title="Create Group"
- onClick={props.onClick}
- >
- ➕
- </button>
+ <span>{props.name}</span>
+ {props.isGroup ? (
+ <span></span>
+ ) : (
+ <button
+ className="create-btn"
+ title="Create Group"
+ onClick={props.onClick}
+ >
+ ➕
+ </button>
+ )}
</div>
);
};
修复属性问题,src/components/create-group.jsx:
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import "./CreateGroup.css";
const CreateGroup = (props) => {
- const [userSids, setUserSids] = useState([]);
+ const [userSids, setUserSids] = useState(props.pal ? [props.pal.sid] : []);
const [groupName, setGroupName] = useState("");
const handleChange = (e) => {
@@ -25,10 +25,11 @@ const CreateGroup = (props) => {
onChange={(e) => setGroupName(e.target.value)}
/>
{props.contacts.map((e, i) => (
- <label for={`option${i}`} className="contact-label">
+ <label key={i} htmlFor={`option${i}`} className="contact-label">
<input
type="checkbox"
id={`option${i}`}
+ defaultChecked={props.pal.sid === e.sid}
value={e.sid}
onChange={handleChange}
/>
调整 src/components/contact.jsx:
@@ -1,13 +1,9 @@
import React from "react";
import "./Contact.css";
-import clsx from "clsx";
const Contact = (props) => {
return (
- <div
- className={clsx("contact", { offline: props.isOffline })}
- onClick={props.onClick}
- >
+ <div className="contact" onClick={props.onClick}>
<div className="name truncate">{props.username}</div>
<div className="last-message truncate">
{props.message || "[no messages]"}
变更 src/App.js:
@@ -5,6 +5,7 @@ import Message from "./components/message";
import TitleBar from "./components/title-bar";
import LoginForm from "./components/login-form";
import CreateGroup from "./components/create-group";
+import Group from "./components/group";
import clsx from "clsx";
import io from "socket.io-client";
@@ -29,6 +30,7 @@ function App() {
const [contacts, setContacts] = useState([]);
const [groups, setGroups] = useState([]);
const [contactMessages, setContactMessages] = useState({});
+ const [groupMessages, setGroupMessages] = useState({});
const [pickedContact, setPickedContact] = useState(null);
const [typedContent, setTypedContent] = useState("");
const resultEndRef = useRef(null);
@@ -52,32 +54,67 @@ function App() {
return { ...cm, [from]: newMessages };
});
});
+ socket.on("create-group", (data) => {
+ const { name, id } = data;
+ setGroups((gs) => [...gs, { name, id }]);
+ setGroupMessages((gm) => {
+ return { ...gm, [id]: [] };
+ });
+ });
+ socket.on("group-chat", (data) => {
+ const { speaker, msg, room: roomId } = data;
+ const entry = { name: speaker, message: msg, isSelf: false };
+ setGroupMessages((gm) => {
+ const oldMessages = gm[roomId] || [];
+ const newMessages = [...oldMessages, entry];
+ return { ...gm, [roomId]: newMessages };
+ });
+ });
socket.emit("user-join", user);
setConn(socket);
}, [user]);
useEffect(() => {
if (!pickedContact) return;
- const messages = contactMessages[pickedContact.sid] || [];
+ const messages =
+ contactMessages[pickedContact.sid] ||
+ groupMessages[pickedContact.id] ||
+ [];
if (messages.length > 0)
resultEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [contactMessages, pickedContact]);
+ }, [contactMessages, groupMessages, pickedContact]);
const login = (emoji, name) => {
setUser({ emoji, name });
setLogged(true);
};
- const chat = (toSid, message) => {
+ const chat = (to, message) => {
if (!conn || !message.trim()) {
return;
}
- const entry = { name: user.name, message, isSelf: true };
- conn.emit("chat", { to: toSid, from: conn.id, msg: message });
- setContactMessages((cm) => {
- const oldMessages = cm[toSid] || [];
- const newMessages = [...oldMessages, entry];
- return { ...cm, [toSid]: newMessages };
- });
+ const fullName = user.emoji + " " + user.name;
+ const entry = { name: fullName, message, isSelf: true };
+ if (isGroupChatting) {
+ const roomId = to;
+ conn.emit("group-chat", {
+ room: roomId,
+ speaker: fullName,
+ msg: message,
+ });
+ setGroupMessages((gm) => {
+ const oldMessages = gm[roomId] || [];
+ const newMessages = [...oldMessages, entry];
+ return { ...gm, [roomId]: newMessages };
+ });
+ } else {
+ conn.emit("chat", { to, from: conn.id, msg: message });
+ setContactMessages((cm) => {
+ const oldMessages = cm[to] || [];
+ const newMessages = [...oldMessages, entry];
+ return { ...cm, [to]: newMessages };
+ });
+ }
+
setTypedContent("");
};
const createGroup = (userSids, groupName) => {
@@ -97,7 +134,7 @@ function App() {
conn.emit("create-group", { sids: userSids, name: groupName, id: roomId });
setCreatingGroup(false);
setIsGroupChatting(true);
- setGroups([...groups, { name: groupName, id: roomId }]);
+ setPickedContact({ name: groupName, id: roomId });
};
const lastMessage = (messages) => {
if (!messages) {
@@ -123,6 +160,10 @@ function App() {
className={clsx("segment left-seg", {
picked: !isGroupChatting,
})}
+ onClick={() => {
+ setIsGroupChatting(false);
+ setPickedContact(null);
+ }}
>
Chat
</span>
@@ -130,41 +171,74 @@ function App() {
className={clsx("segment right-seg", {
picked: isGroupChatting,
})}
+ onClick={() => {
+ setIsGroupChatting(true);
+ setPickedContact(null);
+ }}
>
Groups
</span>
</div>
<div className="card">
<div className="contacts">
- {contacts.map((e) => (
- <Contact
- key={e.sid}
- username={e.emoji + " " + e.name}
- message={lastMessage(contactMessages[e.sid])}
- onClick={() => {
- setPickedContact(e);
- }}
- />
- ))}
+ {isGroupChatting
+ ? groups.map((e) => (
+ <Group
+ key={e.id}
+ name={e.name}
+ message={lastMessage(groupMessages[e.id])}
+ onClick={() => {
+ setPickedContact(e);
+ }}
+ />
+ ))
+ : contacts.map((e) => (
+ <Contact
+ key={e.sid}
+ username={e.emoji + " " + e.name}
+ message={lastMessage(contactMessages[e.sid])}
+ onClick={() => {
+ setPickedContact(e);
+ }}
+ />
+ ))}
</div>
<div className="main">
{pickedContact ? (
<>
<TitleBar
- username={pickedContact.emoji + " " + pickedContact.name}
+ name={
+ isGroupChatting
+ ? pickedContact.name
+ : pickedContact.emoji + " " + pickedContact.name
+ }
onClick={() => setCreatingGroup(true)}
+ isGroup={isGroupChatting}
/>
<div className="messages">
- {(contactMessages[pickedContact.sid] || []).map(
- (e, i) => (
- <Message
- key={i}
- username={e.isSelf ? user.name : pickedContact.name}
- message={e.message}
- isSelf={e.isSelf}
- />
- )
- )}
+ {isGroupChatting
+ ? (groupMessages[pickedContact.id] || []).map(
+ (e, i) => (
+ <Message
+ key={i}
+ username={e.name}
+ message={e.message}
+ isSelf={e.isSelf}
+ />
+ )
+ )
+ : (contactMessages[pickedContact.sid] || []).map(
+ (e, i) => (
+ <Message
+ key={i}
+ username={
+ e.isSelf ? user.name : pickedContact.name
+ }
+ message={e.message}
+ isSelf={e.isSelf}
+ />
+ )
+ )}
<div ref={resultEndRef}></div>
</div>
<div className="edit">
@@ -175,7 +249,12 @@ function App() {
onChange={(e) => setTypedContent(e.target.value)}
onKeyUp={(e) => {
if (e.ctrlKey && e.key === "Enter") {
- chat(pickedContact.sid, typedContent);
+ chat(
+ isGroupChatting
+ ? pickedContact.id
+ : pickedContact.sid,
+ typedContent
+ );
}
}}
/>
@@ -183,7 +262,12 @@ function App() {
<button
className="send-btn"
onClick={() => {
- chat(pickedContact.sid, typedContent);
+ chat(
+ isGroupChatting
+ ? pickedContact.id
+ : pickedContact.sid,
+ typedContent
+ );
}}
>
Send
@@ -214,6 +298,7 @@ function App() {
{creatingGroup && (
<CreateGroup
contacts={contacts}
+ pal={pickedContact}
callback={createGroup}
close={() => setCreatingGroup(false)}
/>
socket.on("create-group", ...)
接收create-group
创建群组消息,然后在本地创建群组。socket.on("group-chat", ...)
接收群聊天消息然后将其推入到 React state 中。conn.emit("group-chat", ...)
发送群聊天消息到 Socket.IO 服务器。