» Make Web Chat App Front-End with React » 2. Development » 2.5 Group Chat: Create Group

Group Chat: Create Group

Add a "+" sign to the right of a conversation title bar.

Changes in src/components/title-bar.jsx:

 import "./TitleBar.css";
 
 const TitleBar = (props) => {
-  return <div className="title">{props.username}</div>;
+  return (
+    <div className="title">
+      <span></span>
+      <span>{props.username}</span>
+      <button
+        className="create-btn"
+        title="Create Group"
+        onClick={props.onClick}
+      >
+        ➕
+      </button>
+    </div>
+  );
 };
 
 export default TitleBar;

Tune styles in src/components/TitleBar.css:

@@ -1,6 +1,17 @@
+@import url(../vars.css);
+
 .title {
   font-size: larger;
   color: black;
   padding: 10px;
   border-bottom: solid 1px lightgray;
+  display: flex;
+  justify-content: space-between;
+}
+
+.create-btn {
+  color: var(--theme-color);
+  border: none;
+  background-color: transparent;
+  cursor: pointer;
 }

Add a create-group modal dialog in src/components/create-group.jsx:

import React, { useState } from "react";
import "./CreateGroup.css";

const CreateGroup = (props) => {
  const [userSids, setUserSids] = useState([]);
  const [groupName, setGroupName] = useState("");

  const handleChange = (e) => {
    const { value, checked } = e.target;
    const newUserSids = userSids.filter((v) => v !== value);
    if (checked) {
      setUserSids([...newUserSids, value]);
    } else {
      setUserSids(newUserSids);
    }
  };
  return (
    <div className="modal">
      <div className="modal-content">
        <div className="group-title">Create Group</div>
        <div className="group-users">
          <input
            className="group-name-ipt"
            placeholder="Group name"
            onChange={(e) => setGroupName(e.target.value)}
          />
          {props.contacts.map((e, i) => (
            <label for={`option${i}`} className="contact-label">
              <input
                type="checkbox"
                id={`option${i}`}
                value={e.sid}
                onChange={handleChange}
              />
              {e.emoji} {e.name}
            </label>
          ))}
        </div>
        <div className="create-buttons">
          <button
            className="send-btn"
            onClick={() => {
              props.callback(userSids, groupName);
            }}
          >
            Create
          </button>
          <button className="cancel-btn" onClick={props.close}>
            Cancel
          </button>
        </div>
      </div>
    </div>
  );
};

export default CreateGroup;

Create Group Dialog

Add styles in src/components/CreateGroup.css:

@import url(../vars.css);

.modal {
  position: fixed; /* Position the element relative to the browser window */
  top: 0;
  left: 0;
  width: 100%; /* Cover the entire viewport width */
  height: 100%; /* Cover the entire viewport height */
  background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent black background */
  z-index: 999; /* Ensure the background is behind other elements */
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  width: 20em;
  height: 20em;
  background-color: aliceblue;
  border-radius: 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.group-title {
  font-size: large;
  text-align: center;
  padding: 0.5em;
  border-bottom: solid 1px lightgray;
}

.group-users {
  padding: 1em;
  flex: 1;
  overflow-y: auto;
}

.create-buttons {
  padding: 0.5em;
  display: flex;
  justify-content: space-evenly;
}

.contact-label {
  display: block;
  font-size: large;
  margin-top: 0.2em;
  cursor: pointer;
}

.group-name-ipt {
  border: none;
  width: 100%;
  font-size: large;
  border-bottom: solid 1px var(--theme-color);
  margin-bottom: 0.5em;
}

.group-name-ipt:focus {
  outline: solid 1px var(--theme-color);
}

.cancel-btn {
  padding: 3px 10px;
  background-color: white;
  border: none;
  border-bottom: solid 2px var(--secondary-color);
  border-radius: 0.25rem;
  cursor: pointer;
  color: orangered;
}

Add createGroup function into src/App.js:

@@ -4,14 +4,30 @@ import Contact from "./components/contact";
 import Message from "./components/message";
 import TitleBar from "./components/title-bar";
 import LoginForm from "./components/login-form";
+import CreateGroup from "./components/create-group";
 import clsx from "clsx";
 import io from "socket.io-client";
 
+function randomString(length) {
+  const characters =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+  let result = "";
+  const charactersLength = characters.length;
+  for (let i = 0; i < length; i++) {
+    result += characters.charAt(Math.floor(Math.random() * charactersLength));
+  }
+  return result;
+}
+
 function App() {
   const [logged, setLogged] = useState(false);
+  const [creatingGroup, setCreatingGroup] = useState(false);
+  const [isGroupChatting, setIsGroupChatting] = useState(false);
+
   const [user, setUser] = useState({ emoji: "", name: "" });
   const [conn, setConn] = useState(null);
   const [contacts, setContacts] = useState([]);
+  const [groups, setGroups] = useState([]);
   const [contactMessages, setContactMessages] = useState({});
   const [pickedContact, setPickedContact] = useState(null);
   const [typedContent, setTypedContent] = useState("");
@@ -64,6 +80,25 @@ function App() {
     });
     setTypedContent("");
   };
+  const createGroup = (userSids, groupName) => {
+    if (!conn) {
+      return;
+    }
+    if (!groupName) {
+      alert("Group name needed.");
+      return;
+    }
+    if (!userSids || userSids.length < 2) {
+      alert("A group takes at least 3 users.");
+      return;
+    }
+    userSids.push(conn.id);
+    const roomId = randomString(6);
+    conn.emit("create-group", { sids: userSids, name: groupName, id: roomId });
+    setCreatingGroup(false);
+    setIsGroupChatting(true);
+    setGroups([...groups, { name: groupName, id: roomId }]);
+  };
   const lastMessage = (messages) => {
     if (!messages) {
       return "";
@@ -74,92 +109,116 @@ function App() {
     return "";
   };
   return (
-    <div className="app">
-      <h1 className={clsx("app-name", { "center-name": !logged })}>
-        Literank Web Chat
-      </h1>
-      {!logged ? (
-        <LoginForm callback={login} />
-      ) : (
-        <>
-          <div className="segments">
-            <span className="segment left-seg picked">Chat</span>
-            <span className="segment right-seg">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);
-                  }}
-                />
-              ))}
+    <>
+      <div className="app">
+        <h1 className={clsx("app-name", { "center-name": !logged })}>
+          Literank Web Chat
+        </h1>
+        {!logged ? (
+          <LoginForm callback={login} />
+        ) : (
+          <>
+            <div className="segments">
+              <span
+                className={clsx("segment left-seg", {
+                  picked: !isGroupChatting,
+                })}
+              >
+                Chat
+              </span>
+              <span
+                className={clsx("segment right-seg", {
+                  picked: isGroupChatting,
+                })}
+              >
+                Groups
+              </span>
             </div>
-            <div className="main">
-              {pickedContact ? (
-                <>
-                  <TitleBar
-                    username={pickedContact.emoji + " " + pickedContact.name}
+            <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);
+                    }}
                   />
-                  <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}
-                      />
-                    ))}
-                    <div ref={resultEndRef}></div>
-                  </div>
-                  <div className="edit">
-                    <textarea
-                      className="edit-box"
-                      placeholder="Type here"
-                      value={typedContent}
-                      onChange={(e) => setTypedContent(e.target.value)}
-                      onKeyUp={(e) => {
-                        if (e.ctrlKey && e.key === "Enter") {
-                          chat(pickedContact.sid, typedContent);
-                        }
-                      }}
+                ))}
+              </div>
+              <div className="main">
+                {pickedContact ? (
+                  <>
+                    <TitleBar
+                      username={pickedContact.emoji + " " + pickedContact.name}
+                      onClick={() => setCreatingGroup(true)}
                     />
-                    <div className="buttons">
-                      <button
-                        className="send-btn"
-                        onClick={() => {
-                          chat(pickedContact.sid, typedContent);
+                    <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}
+                          />
+                        )
+                      )}
+                      <div ref={resultEndRef}></div>
+                    </div>
+                    <div className="edit">
+                      <textarea
+                        className="edit-box"
+                        placeholder="Type here"
+                        value={typedContent}
+                        onChange={(e) => setTypedContent(e.target.value)}
+                        onKeyUp={(e) => {
+                          if (e.ctrlKey && e.key === "Enter") {
+                            chat(pickedContact.sid, typedContent);
+                          }
                         }}
-                      >
-                        Send
-                      </button>
-                      <span className="tip">Ctrl+Enter to send</span>
+                      />
+                      <div className="buttons">
+                        <button
+                          className="send-btn"
+                          onClick={() => {
+                            chat(pickedContact.sid, typedContent);
+                          }}
+                        >
+                          Send
+                        </button>
+                        <span className="tip">Ctrl+Enter to send</span>
+                      </div>
                     </div>
-                  </div>
-                </>
-              ) : (
-                <div className="brand">Literank</div>
-              )}
+                  </>
+                ) : (
+                  <div className="brand">Literank</div>
+                )}
+              </div>
             </div>
-          </div>
-          <div className="status">
-            <span>
-              {user.emoji} {user.name}
-            </span>
-            <div className="connection-status">
-              <span
-                className={clsx("dot", { connected: conn?.connected })}
-              ></span>
-              <span>{conn?.connected ? "Connected" : "Disconnected"}</span>
+            <div className="status">
+              <span>
+                {user.emoji} {user.name}
+              </span>
+              <div className="connection-status">
+                <span
+                  className={clsx("dot", { connected: conn?.connected })}
+                ></span>
+                <span>{conn?.connected ? "Connected" : "Disconnected"}</span>
+              </div>
             </div>
-          </div>
-        </>
+          </>
+        )}
+      </div>
+      {creatingGroup && (
+        <CreateGroup
+          contacts={contacts}
+          callback={createGroup}
+          close={() => setCreatingGroup(false)}
+        />
       )}
-    </div>
+    </>
   );
 }
PrevNext