一对一聊天
联系人列表
选择一个随机 emoji,src/components/login-form.jsx:
@@ -29,7 +29,8 @@ const emojis = [
"🦄", // Unicorn
];
const LoginForm = (props) => {
- const [emoji, setEmoji] = useState("");
+ const rndEmoji = emojis[Math.floor(emojis.length * Math.random())];
+ const [emoji, setEmoji] = useState(rndEmoji);
const [name, setName] = useState("");
const login = () => {
if (!emoji) {
调整 src/components/contact.jsx:
@@ -4,9 +4,14 @@ import clsx from "clsx";
const Contact = (props) => {
return (
- <div className={clsx("contact", { offline: props.isOffline })}>
+ <div
+ className={clsx("contact", { offline: props.isOffline })}
+ onClick={props.onClick}
+ >
<div className="name truncate">{props.username}</div>
- <div className="last-message truncate">{props.message}</div>
+ <div className="last-message truncate">
+ {props.message || "[no messages]"}
+ </div>
</div>
);
};
调整 src/components/Contact.css:
@@ -4,6 +4,7 @@
padding: 6px 10px;
border-bottom: solid 1px var(--theme-color);
text-align: left;
+ cursor: pointer;
}
.name {
从 socket.io 服务端接收实时联系人列表。
更新 src/App.js:
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import "./App.css";
import Contact from "./components/contact";
import Message from "./components/message";
-import TitleBar from "./components/ttile-bar";
+import TitleBar from "./components/title-bar";
import LoginForm from "./components/login-form";
import clsx from "clsx";
import io from "socket.io-client";
@@ -11,6 +11,8 @@ function App() {
const [logged, setLogged] = useState(false);
const [user, setUser] = useState({ emoji: "", name: "" });
const [conn, setConn] = useState(null);
+ const [contacts, setContacts] = useState([]);
+ const [pickedContact, setPickedContact] = useState(null);
const messages = [
{
username: "Alice",
@@ -29,31 +31,19 @@ function App() {
message: "Sure",
},
];
- const contacts = [
- {
- username: "🦁 Alice",
- message: "Sure",
- },
- {
- username: "♣ Cindy",
- message: "Where r u",
- },
- {
- username: "🐯 Doug Smith",
- message: "Hi",
- },
- {
- username: "🐴 Emily",
- message: "How's your assignment? I didn't do much yesterday",
- },
- ];
useEffect(() => {
+ if (!user.name) return;
const socket = io("http://localhost:4000");
socket.on("error", (error) => {
console.error("Socket error:", error);
});
+ socket.on("contacts", (serialUsers) => {
+ const users = new Map(serialUsers);
+ setContacts([...users.values()].filter((e) => e.sid !== socket.id));
+ });
+ socket.emit("user-join", user);
setConn(socket);
- }, []);
+ }, [user]);
const login = (emoji, name) => {
setUser({ emoji, name });
@@ -76,30 +66,42 @@ function App() {
<div className="contacts">
{contacts.map((e) => (
<Contact
- username={e.username}
+ key={e.sid}
+ username={e.emoji + " " + e.name}
message={e.message}
- isOffline={e.username.includes("Emily")}
+ onClick={() => {
+ setPickedContact(e);
+ }}
/>
))}
</div>
<div className="main">
- <TitleBar username={contacts[0].username} />
- <div className="messages">
- {messages.map((e) => (
- <Message
- username={e.username}
- message={e.message}
- isSelf={e.username === "Bob"}
+ {pickedContact ? (
+ <>
+ <TitleBar
+ username={pickedContact.emoji + " " + pickedContact.name}
/>
- ))}
- </div>
- <div className="edit">
- <textarea className="edit-box" placeholder="Type here" />
- <div className="buttons">
- <button className="send-btn">Send</button>
- <span className="tip">Ctrl+Enter to send</span>
- </div>
- </div>
+ <div className="messages">
+ {messages.map((e) => (
+ <Message
+ key={e.message}
+ username={e.username}
+ message={e.message}
+ isSelf={e.username === "Bob"}
+ />
+ ))}
+ </div>
+ <div className="edit">
+ <textarea className="edit-box" placeholder="Type here" />
+ <div className="buttons">
+ <button className="send-btn">Send</button>
+ <span className="tip">Ctrl+Enter to send</span>
+ </div>
+ </div>
+ </>
+ ) : (
+ <div className="brand">Literank</div>
+ )}
</div>
</div>
<div className="status">
调整风格,src/App.css:
@@ -60,11 +60,13 @@ body {
.contacts {
flex: 1;
+ overflow-y: auto;
+ max-height: var(--max-h);
}
.main {
flex: 4;
- height: 60vh;
+ height: var(--max-h);
background-color: whitesmoke;
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
@@ -72,6 +74,16 @@ body {
flex-direction: column;
}
+.brand {
+ color: rgba(0, 0, 0, 0.08);
+ user-select: none;
+ font-size: 3em;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
.main .messages {
flex: 6;
padding: 10px;
添加新的变量,src/vars.css:
@@ -3,4 +3,5 @@
--text-color: #ffffff;
--theme-color: #005b41;
--secondary-color: #232d3f;
+ --max-h: 60vh;
}
修复风格问题
调整 src/components/login-form.jsx:
@@ -77,7 +77,7 @@ const LoginForm = (props) => {
/>
</div>
</div>
- <div className="buttons">
+ <div className="login-buttons">
<button className="send-btn" onClick={login}>
Login
</button>
调整 src/components/Login.css:
@@ -35,6 +35,6 @@
text-align: center;
}
-.buttons {
+.login-buttons {
margin-top: 2em;
}
聊天消息
限制消息气泡大小,src/components/Message.css:
@@ -7,6 +7,8 @@
.message {
width: max-content;
+ margin-bottom: 0.2em;
+ max-width: 25em;
}
.self-side {
修改 src/App.js:
diff --git a/src/App.js b/src/App.js
index ed953c2..1faa05a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import "./App.css";
import Contact from "./components/contact";
import Message from "./components/message";
@@ -12,25 +12,12 @@ function App() {
const [user, setUser] = useState({ emoji: "", name: "" });
const [conn, setConn] = useState(null);
const [contacts, setContacts] = useState([]);
+ const [contactMessages, setContactMessages] = useState({});
const [pickedContact, setPickedContact] = useState(null);
- const messages = [
- {
- username: "Alice",
- message: "Hello there",
- },
- {
- username: "Bob",
- message: "How's everything?",
- },
- {
- username: "Alice",
- message: "Great! Wanna have a drink?",
- },
- {
- username: "Bob",
- message: "Sure",
- },
- ];
+ const [typedContent, setTypedContent] = useState("");
+ const [messages, setMessages] = useState([]);
+ const resultEndRef = useRef(null);
+
useEffect(() => {
if (!user.name) return;
const socket = io("http://localhost:4000");
@@ -41,14 +28,38 @@ function App() {
const users = new Map(serialUsers);
setContacts([...users.values()].filter((e) => e.sid !== socket.id));
});
+ socket.on("chat", (data) => {
+ const { from, msg } = data;
+ setMessages((m) => [...m, { name: "", message: msg, isSelf: false }]);
+ setContactMessages((m) => {
+ return { ...m, [from]: msg };
+ });
+ });
socket.emit("user-join", user);
setConn(socket);
}, [user]);
+ useEffect(() => {
+ if (messages.length > 0)
+ resultEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
const login = (emoji, name) => {
setUser({ emoji, name });
setLogged(true);
};
+ const chat = (toSid, message) => {
+ if (!conn) {
+ return;
+ }
+ setMessages((m) => [...m, { name: user.name, message, isSelf: true }]);
+ conn.emit("chat", { to: toSid, from: conn.id, msg: message });
+ setContactMessages((m) => {
+ return { ...m, [toSid]: message };
+ });
+ setTypedContent("");
+ };
return (
<div className="app">
<h1 className={clsx("app-name", { "center-name": !logged })}>
@@ -68,9 +79,10 @@ function App() {
<Contact
key={e.sid}
username={e.emoji + " " + e.name}
- message={e.message}
+ message={contactMessages[e.sid] || ""}
onClick={() => {
setPickedContact(e);
+ setMessages([]);
}}
/>
))}
@@ -82,19 +94,37 @@ function App() {
username={pickedContact.emoji + " " + pickedContact.name}
/>
<div className="messages">
- {messages.map((e) => (
+ {messages.map((e, i) => (
<Message
- key={e.message}
- username={e.username}
+ key={i}
+ username={e.isSelf ? user.name : pickedContact.name}
message={e.message}
- isSelf={e.username === "Bob"}
+ isSelf={e.isSelf}
/>
))}
+ <div ref={resultEndRef}></div>
</div>
<div className="edit">
- <textarea className="edit-box" placeholder="Type here" />
+ <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 className="buttons">
- <button className="send-btn">Send</button>
+ <button
+ className="send-btn"
+ onClick={() => {
+ chat(pickedContact.sid, typedContent);
+ }}
+ >
+ Send
+ </button>
<span className="tip">Ctrl+Enter to send</span>
</div>
</div>
- 使用
socket.on("chat", ...)
接收服务端的聊天消息。 - 使用
socket.emit("chat", ...)
发送聊天消息到服务端。 - 添加点击处理逻辑到 Send 按钮和 Ctrl+Enter 按键事件。
- 添加 2 个 React State 来辅助渲染消息。
调整风格,src/App.css:
@@ -87,6 +87,7 @@ body {
.main .messages {
flex: 6;
padding: 10px;
+ overflow-y: auto;
}
.main .edit {
现在你可以和朋友聊天啦。尝试进行一次对话:
多个对话
更新 src/App.js:
@@ -15,7 +15,6 @@ function App() {
const [contactMessages, setContactMessages] = useState({});
const [pickedContact, setPickedContact] = useState(null);
const [typedContent, setTypedContent] = useState("");
- const [messages, setMessages] = useState([]);
const resultEndRef = useRef(null);
useEffect(() => {
@@ -30,9 +29,11 @@ function App() {
});
socket.on("chat", (data) => {
const { from, msg } = data;
- setMessages((m) => [...m, { name: "", message: msg, isSelf: false }]);
- setContactMessages((m) => {
- return { ...m, [from]: msg };
+ const entry = { name: "", message: msg, isSelf: false };
+ setContactMessages((cm) => {
+ const oldMessages = cm[from] || [];
+ const newMessages = [...oldMessages, entry];
+ return { ...cm, [from]: newMessages };
});
});
socket.emit("user-join", user);
@@ -40,25 +41,38 @@ function App() {
}, [user]);
useEffect(() => {
+ if (!pickedContact) return;
+ const messages = contactMessages[pickedContact.sid] || [];
if (messages.length > 0)
resultEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [messages]);
+ }, [contactMessages, pickedContact]);
const login = (emoji, name) => {
setUser({ emoji, name });
setLogged(true);
};
const chat = (toSid, message) => {
- if (!conn) {
+ if (!conn || !message.trim()) {
return;
}
- setMessages((m) => [...m, { name: user.name, message, isSelf: true }]);
+ const entry = { name: user.name, message, isSelf: true };
conn.emit("chat", { to: toSid, from: conn.id, msg: message });
- setContactMessages((m) => {
- return { ...m, [toSid]: message };
+ setContactMessages((cm) => {
+ const oldMessages = cm[toSid] || [];
+ const newMessages = [...oldMessages, entry];
+ return { ...cm, [toSid]: newMessages };
});
setTypedContent("");
};
+ const lastMessage = (messages) => {
+ if (!messages) {
+ return "";
+ }
+ if (messages.length > 0) {
+ return messages[messages.length - 1].message;
+ }
+ return "";
+ };
return (
<div className="app">
<h1 className={clsx("app-name", { "center-name": !logged })}>
@@ -78,10 +92,9 @@ function App() {
<Contact
key={e.sid}
username={e.emoji + " " + e.name}
- message={contactMessages[e.sid] || ""}
+ message={lastMessage(contactMessages[e.sid])}
onClick={() => {
setPickedContact(e);
- setMessages([]);
}}
/>
))}
@@ -93,7 +106,7 @@ function App() {
username={pickedContact.emoji + " " + pickedContact.name}
/>
<div className="messages">
- {messages.map((e, i) => (
+ {(contactMessages[pickedContact.sid] || []).map((e, i) => (
<Message
key={i}
username={e.isSelf ? user.name : pickedContact.name}
使用 contactMessages
state 来保存所有对话消息。
Loading...
> 此处输出代码运行结果