From 6614b071606d6958a9600a79313007bf8b1a4095 Mon Sep 17 00:00:00 2001 From: yeonnnnjs <sungyeon52@gmail.com> Date: Sun, 10 Dec 2023 23:50:25 +0900 Subject: [PATCH] feat : Add paging --- src/API/UserService.js | 1 - src/Components/Chat/ChatScreen.js | 131 +++++++++++++++++++-------- src/Components/Chat/Message.js | 11 ++- src/Components/Chat/UserMessage.js | 4 +- src/Components/Common/CommonForm.js | 1 - src/Components/Common/dotenv.js | 3 +- src/Components/User/FriendsList.js | 3 +- src/Components/User/ProfileUpdate.js | 6 +- src/Components/User/UserComponent.js | 3 +- src/Components/User/UserList.js | 2 +- src/Components/User/UserProfile.js | 3 +- src/Contexts/SocketContext.js | 3 +- src/Pages/Chat/Chat.js | 4 +- src/css/components/chat.css | 1 + src/css/components/chatHeader.css | 8 +- src/css/components/message.css | 40 +++++--- src/css/components/styles.css | 20 ---- 17 files changed, 153 insertions(+), 91 deletions(-) diff --git a/src/API/UserService.js b/src/API/UserService.js index 49921c2..f7c858c 100644 --- a/src/API/UserService.js +++ b/src/API/UserService.js @@ -3,7 +3,6 @@ import env from '../Components/Common/dotenv'; const API_User_URL = env.REACT_APP_API_BASE_URL + "/user"; const accessToken = localStorage.getItem("accessToken"); - const UserService = { getFriendList: async () => { try { diff --git a/src/Components/Chat/ChatScreen.js b/src/Components/Chat/ChatScreen.js index 133130e..c973512 100644 --- a/src/Components/Chat/ChatScreen.js +++ b/src/Components/Chat/ChatScreen.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { useSocket } from "../../Contexts/SocketContext"; -import chatService from "../../API/ChatService" -import userService from "../../API/UserService" +import chatService from "../../API/ChatService"; +import userService from "../../API/UserService"; import Message from "./Message"; function ChatScreen({ roomId, userInfo }) { @@ -10,18 +10,30 @@ function ChatScreen({ roomId, userInfo }) { const [message, setMessage] = useState(""); const [userId, setUserId] = useState(); const [startId, setStartId] = useState(null); + const [isNearBottom, setIsNearBottom] = useState(true); + const [scrollPosition, setScrollPosition] = useState(null); useEffect(() => { fetchUserInfo(); - fetchMesssages(); - + fetchMessages(); sendMessageWhenReady(socket, { type: "joinRoom", data: { roomId } }); + socket.addEventListener("message", handleSocketMessage); + return () => { socket.removeEventListener("message", handleSocketMessage); }; }, []); + useEffect(() => { + if(scrollPosition == 0) { + fetchMessages(); + window.scrollTo({ + top: document.documentElement.scrollHeight-10, + }); + } + }, [scrollPosition]); + function sendMessageWhenReady(client, message) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); @@ -30,10 +42,18 @@ function ChatScreen({ roomId, userInfo }) { } } - const fetchMesssages = async () => { + const fetchMessages = async () => { const response = await chatService.getMessages(roomId, startId); setStartId(response.nextId); - setMessages(response.messages); + setMessages((prevMessages) => { + let newMessages = []; + if (startId) { + newMessages = response.messages.concat(prevMessages); + } else { + newMessages = response.messages; + } + return newMessages; + }); }; const fetchUserInfo = async () => { @@ -41,18 +61,14 @@ function ChatScreen({ roomId, userInfo }) { setUserId(response.id); }; - // useEffect(() => { - // console.log(messages); - // }, [messages]); - const handleSocketMessage = (event) => { const receivedMessage = JSON.parse(event.data); switch (receivedMessage.type) { case "getmsg": - setMessages((messages) => { - return !messages ? [receivedMessage.data] : [...messages, receivedMessage.data]; - }); + setMessages((messages) => { + return !messages ? [receivedMessage.data] : [...messages, receivedMessage.data]; + }); break; default: break; @@ -69,8 +85,9 @@ function ChatScreen({ roomId, userInfo }) { data: { userName: userId, roomId, message }, }); setMessage(""); + handleScrollToBottom(true); }; - + const getUserInfo = (id) => { for (let el of userInfo) { if (el.id == id) { @@ -79,33 +96,71 @@ function ChatScreen({ roomId, userInfo }) { } }; + const handleScroll = () => { + setScrollPosition(window.scrollY); + const { scrollTop, scrollHeight, clientHeight } = document.documentElement; + const num = scrollTop + clientHeight >= scrollHeight - 100; + setIsNearBottom(num); + }; + + useEffect(() => { + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + useEffect(() => { + handleNewMessage(); + }, [messages]); + + const handleScrollToBottom = () => { + window.scrollTo({ + top: document.documentElement.scrollHeight, + }); + }; + + const handleNewMessage = () => { + if(isNearBottom) { + window.scrollTo({ + top: document.documentElement.scrollHeight, + }); + } + }; + return ( - <main className="main-screen main-chat"> - {Array.isArray(messages) && - messages.map((msg) => ( - <Message - message={msg} - userId={userId} - senderInfo={getUserInfo(msg.sender)} - /> - ))} - <div className="reply"> - <div className="reply__column"> - <input - type="text" - placeholder="Write a message..." - value={message} - onChange={handleMessageChange} - onKeyPress={(event) => { - if (event.key === "Enter") { - handleSendMessage(); - } - }} - /> - </div> + <main> + <div className="main-screen main-chat"> + {!isNearBottom && ( + <button className="scroll-to-bottom-btn" onClick={handleScrollToBottom}> + 이동 + </button> + )} + {Array.isArray(messages) && + messages.map((msg) => ( + <Message + key={msg.id} + message={msg} + userId={userId} + senderInfo={getUserInfo(msg.sender)} + /> + ))} </div> + <footer className="reply"> + <input + type="text" + placeholder="Write a message..." + value={message} + onChange={handleMessageChange} + onKeyPress={(event) => { + if (event.key === "Enter") { + handleSendMessage(); + } + }} + /> + </footer> </main> ); } -export default ChatScreen; +export default ChatScreen; \ No newline at end of file diff --git a/src/Components/Chat/Message.js b/src/Components/Chat/Message.js index c8b1995..8d81803 100644 --- a/src/Components/Chat/Message.js +++ b/src/Components/Chat/Message.js @@ -1,5 +1,6 @@ import React from "react"; import "../../css/components/message.css"; +import env from "../Common/dotenv"; function Message({ message, userId, senderInfo }) { const isOwnMessage = message.sender === userId; @@ -8,20 +9,20 @@ function Message({ message, userId, senderInfo }) { }`; return ( <div className={messageRowClass}> - {!isOwnMessage && ( + {!isOwnMessage ? ( <img src={ senderInfo.profileImage == null || senderInfo.profileImage == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : senderInfo.profileImage } alt={senderInfo.name} /> - )} + ):""} <div className="message-row__content"> - {!isOwnMessage && ( + {!isOwnMessage ? ( <span className="message__author">{senderInfo.name}</span> - )} + ):""} <div className="message__info"> <span className="message__bubble">{message.content}</span> <span className="message__time">{message.timestamp}</span> diff --git a/src/Components/Chat/UserMessage.js b/src/Components/Chat/UserMessage.js index 66e77ff..e141a72 100644 --- a/src/Components/Chat/UserMessage.js +++ b/src/Components/Chat/UserMessage.js @@ -1,3 +1,5 @@ +import env from "../Common/dotenv" + function UserMessage({ avatar, name, @@ -14,7 +16,7 @@ function UserMessage({ <img src={ avatar == null || avatar == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : avatar } className="user-component__avatar user-component__avatar--xl" diff --git a/src/Components/Common/CommonForm.js b/src/Components/Common/CommonForm.js index 45bb78b..07c3788 100644 --- a/src/Components/Common/CommonForm.js +++ b/src/Components/Common/CommonForm.js @@ -3,7 +3,6 @@ import { useState } from "react"; import { EmailButton } from '../Auth/Signin/EmailButton.js'; import authService from "../../API/AuthService.js"; - function CommonForm({ onSubmit, buttonText, fields, showVerificationButton, setWarningMessage }) { const [emailMessage, setEmailMessage] = useState("전송"); const [emailValue, setEmailValue] = useState(""); diff --git a/src/Components/Common/dotenv.js b/src/Components/Common/dotenv.js index 66caa7b..c4b158d 100644 --- a/src/Components/Common/dotenv.js +++ b/src/Components/Common/dotenv.js @@ -1,6 +1,7 @@ export const loadEnv = { REACT_APP_API_BASE_URL: "http://localhost:8080/api", // include /api prefix - REACT_APP_IMAGE_BASE_URL: "http://localhost:8040" + REACT_APP_IMAGE_BASE_URL: "http://localhost:8040", + REACT_APP_SOCKET_BASE_URL: "ws://localhost:3001" }; export default loadEnv; \ No newline at end of file diff --git a/src/Components/User/FriendsList.js b/src/Components/User/FriendsList.js index 1debb41..5b76f18 100644 --- a/src/Components/User/FriendsList.js +++ b/src/Components/User/FriendsList.js @@ -4,6 +4,7 @@ import UserService from "../../API/UserService"; import ChatService from "../../API/ChatService"; import AuthService from "../../API/AuthService"; import Comment from "../../assets/speech-bubble.png"; +import env from "../Common/dotenv" import { useEffect, useState } from "react"; @@ -86,7 +87,7 @@ function FriendsList() { key={friend.id} avatar={ friend.profileImage == null || friend.profileImage == "" - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : friend.profileImage } name={friend.name} diff --git a/src/Components/User/ProfileUpdate.js b/src/Components/User/ProfileUpdate.js index d1781ee..25e0adc 100644 --- a/src/Components/User/ProfileUpdate.js +++ b/src/Components/User/ProfileUpdate.js @@ -1,6 +1,6 @@ import { useRef, useEffect, useState } from "react"; import UserService from "../../API/UserService"; -const baseURL = "http://localhost:8040"; +import env from "../Common/dotenv" function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { const [image, setImage] = useState(null); @@ -20,7 +20,7 @@ function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { formData.append("image", file); try { - const response = await fetch(`${baseURL}/api/upload`, { + const response = await fetch(`${env.REACT_APP_IMAGE_BASE_URL}/api/upload`, { method: "POST", body: formData, }); @@ -30,7 +30,7 @@ function ProfileUpdate({ onClose, origin_image, origin_comment, name }) { } const data = await response.json(); - setImageUrl(`${baseURL}${data.imageUrl}`); // 서버로부터 받은 이미지 URL + setImageUrl(`${env.REACT_APP_IMAGE_BASE_URL}${data.imageUrl}`); // 서버로부터 받은 이미지 URL } catch (error) { console.error(error); } diff --git a/src/Components/User/UserComponent.js b/src/Components/User/UserComponent.js index cc35fb6..481e2f5 100644 --- a/src/Components/User/UserComponent.js +++ b/src/Components/User/UserComponent.js @@ -1,4 +1,5 @@ import React from "react"; +import env from "../Common/dotenv" import "../../css/components/userProfile.css"; function UserComponent({ avatar, name, subtitle, additionalContent }) { @@ -14,7 +15,7 @@ function UserComponent({ avatar, name, subtitle, additionalContent }) { <img src={ !avatar - ? "http://localhost:8040/uploads/default-profile.png" + ? `${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png` : avatar } className="user-component__avatar" diff --git a/src/Components/User/UserList.js b/src/Components/User/UserList.js index 850cf7d..1302216 100644 --- a/src/Components/User/UserList.js +++ b/src/Components/User/UserList.js @@ -5,7 +5,7 @@ import AuthService from "../../API/AuthService"; import Add from "../../assets/add-friend-icon.png"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; function UserList() { const [showAddedFriends, setShowAddedFriends] = useState(false); diff --git a/src/Components/User/UserProfile.js b/src/Components/User/UserProfile.js index 0b18665..f5b239a 100644 --- a/src/Components/User/UserProfile.js +++ b/src/Components/User/UserProfile.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import UserService from "../../API/UserService"; import AuthService from "../../API/AuthService"; import ProfileComponent from "./ProfileComponent"; +import env from "../Common/dotenv" function UserProfile() { const accessToken = localStorage.getItem("accessToken"); @@ -17,7 +18,7 @@ function UserProfile() { if (response) { setSubtitle(response.comment); if (response.profileImage == null || response.profileImage == "") { - setUserProfile("http://localhost:8040/uploads/default-profile.png"); + setUserProfile(`${env.REACT_APP_IMAGE_BASE_URL}/uploads/default-profile.png`); } else { setUserProfile(response.profileImage); } diff --git a/src/Contexts/SocketContext.js b/src/Contexts/SocketContext.js index 8b3612f..1a96a5f 100644 --- a/src/Contexts/SocketContext.js +++ b/src/Contexts/SocketContext.js @@ -1,8 +1,9 @@ import React, { createContext, useContext, useEffect } from "react"; +import env from "../Components/Common/dotenv" const SocketContext = createContext(); -const socket = new WebSocket("ws://localhost:3001"); +const socket = new WebSocket(`${env.REACT_APP_SOCKET_BASE_URL}`); export function useSocket() { return useContext(SocketContext); diff --git a/src/Pages/Chat/Chat.js b/src/Pages/Chat/Chat.js index 971fc25..b0ff269 100644 --- a/src/Pages/Chat/Chat.js +++ b/src/Pages/Chat/Chat.js @@ -16,10 +16,10 @@ function Chat() { useEffect(() => { fetchAllUserInfo(); }, []); - + return ( <WebSocketProvider> - <div id="chat-screen"> + <div> <ChatHeader roomId={roomId} /> {userInfo ? <ChatScreen roomId={roomId} userInfo={userInfo} /> : ""} </div> diff --git a/src/css/components/chat.css b/src/css/components/chat.css index b53abcb..c1af22b 100644 --- a/src/css/components/chat.css +++ b/src/css/components/chat.css @@ -107,6 +107,7 @@ position: fixed; bottom: 0; width: 100%; + height: 3%; background-color: white; display: flex; justify-content: space-between; diff --git a/src/css/components/chatHeader.css b/src/css/components/chatHeader.css index 31ddc8d..eddc248 100644 --- a/src/css/components/chatHeader.css +++ b/src/css/components/chatHeader.css @@ -1,8 +1,14 @@ .alt-header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3%; + z-index: 100; display: flex; align-items: center; justify-content: space-between; - padding: 1rem; + padding: 2vh; background-color: #0054a6; /* 아주대학교 색상인 파란색으로 변경 */ color: #fff; } diff --git a/src/css/components/message.css b/src/css/components/message.css index 3d840bc..ee526c0 100644 --- a/src/css/components/message.css +++ b/src/css/components/message.css @@ -1,8 +1,12 @@ .main-screen.main-chat { /* background-color: #f9f9f9; */ - padding: 20px; + padding-left: 3vw; + padding-right: 3vw; overflow-y: auto; + margin-top: 7vh; + margin-bottom: 7vh; } + .message { margin-bottom: 20px; padding: 10px; @@ -16,15 +20,6 @@ margin-bottom: 5px; } -.reply { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 10px; - background-color: #fff; - box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); -} .reply__column { display: flex; align-items: center; @@ -97,10 +92,29 @@ background-color: #fee500; } +.scroll-to-bottom-btn { + position: fixed; + bottom: 5%; + left: 50%; + transform: translateX(-50%); + background-color: #0054a6; + color: #fff; + border: none; + border-radius: 25%; + padding: 1%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + margin-bottom: 1%; +} + .reply { - /* display: flex; */ - justify-content: space-between; - padding: 10px; + display: flex; + justify-content: start; + padding: 1vh; + height: 3vh; background-color: #fff; position: fixed; bottom: 0; diff --git a/src/css/components/styles.css b/src/css/components/styles.css index 0ac4d4b..44fbad6 100644 --- a/src/css/components/styles.css +++ b/src/css/components/styles.css @@ -6,26 +6,6 @@ body, h1, p, input, button, span { -moz-osx-font-smoothing: grayscale; } -.status-bar { - background-color: #0054a6; - color: white; - padding: 10px 20px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; -} - -.status-bar__column span { - display: flex; - align-items: center; -} - -.status-bar__column span:not(:last-child) { - margin-right: 10px; -} - - input::placeholder { color: #bbb; } -- GitLab