» React创建在线聊天软件 Web Chat App 前端 » 2. 开发 » 2.6 群聊天:群消息

群聊天:群消息

添加 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 服务器。
上页下页