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;
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>
+ </>
);
}