From e83c3879753c50b99046e1ddb9ecb5420ec13f53 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 5 Oct 2024 20:05:28 +0800 Subject: [PATCH 1/3] feature: optimize room logic to enhance user experience --- components/rooms/RoomButtonGroup.tsx | 1 + components/shared/Sidebar.tsx | 78 +++++++++++++++++++--------- containers/room/RoomListView.tsx | 4 ++ hooks/useCookie.ts | 6 +++ hooks/useUser.ts | 16 +++++- pages/index.tsx | 24 ++++++++- pages/rooms/[roomId]/index.tsx | 6 +++ 7 files changed, 107 insertions(+), 28 deletions(-) diff --git a/components/rooms/RoomButtonGroup.tsx b/components/rooms/RoomButtonGroup.tsx index bfd07215..de81896e 100644 --- a/components/rooms/RoomButtonGroup.tsx +++ b/components/rooms/RoomButtonGroup.tsx @@ -18,6 +18,7 @@ function RoomButtonGroup(props: RoomButtonGroupProps) { isHost, isReady, } = props; + return (
- - ))} + + + ))} {currentUser && (
Massive Monster
-
AZUL ({imgAlt})
+
{imgAlt}
4.6 * * * * * (14)
@@ -128,6 +131,23 @@ const TabPaneContent = (tabItem: TabItemType) => { }; export default function Home() { + const { fetch } = useRequest(); + const [gameList, setGameList] = useState([]); + + useEffect(() => { + async function handleGetAllGame() { + const result = await fetch(getAllGamesEndpoint()); + setGameList( + result.map((gameItem) => ({ + link: `/rooms/${gameItem.id}`, + imgUrl: gameItem.img, + imgAlt: gameItem.name, + })) + ); + } + handleGetAllGame(); + }, [fetch]); + return (
@@ -141,7 +161,7 @@ export default function Home() {
diff --git a/pages/rooms/[roomId]/index.tsx b/pages/rooms/[roomId]/index.tsx index ba8dfbbf..b0ae9c3f 100644 --- a/pages/rooms/[roomId]/index.tsx +++ b/pages/rooms/[roomId]/index.tsx @@ -22,6 +22,7 @@ import { playerCancelReady, startGame, } from "@/requests/rooms"; +import useUser from "@/hooks/useUser"; type User = Omit; @@ -38,6 +39,7 @@ export default function Room() { } = useRoom(); const { socket } = useSocketCore(); const { currentUser, token } = useAuth(); + const { updateRoomId } = useUser(); const { Popup, firePopup } = usePopup(); const { fetch } = useRequest(); const { query, replace } = useRouter(); @@ -70,6 +72,7 @@ export default function Room() { socket.on(SOCKET_EVENT.USER_LEFT, ({ user }: { user: User }) => { if (user.id === currentUser.id) { + updateRoomId(); firePopup({ title: `你已被踢出房間`, onConfirm: () => replace("/"), @@ -105,6 +108,7 @@ export default function Room() { }); socket.on(SOCKET_EVENT.ROOM_CLOSED, () => { + updateRoomId(); firePopup({ title: `房間已關閉!`, onConfirm: () => replace("/"), @@ -149,6 +153,7 @@ export default function Room() { try { await fetch(closeRoom(roomId)); replace("/rooms"); + updateRoomId(); } catch (err) { firePopup({ title: "error!" }); } @@ -167,6 +172,7 @@ export default function Room() { try { await fetch(leaveRoom(roomId)); replace("/rooms"); + updateRoomId(); } catch (err) { firePopup({ title: "error!" }); } From f28e5f935e414d97c55396f3aba0a017f447dbf5 Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 5 Oct 2024 20:07:27 +0800 Subject: [PATCH 2/3] feature: integrate new chat component with web socket --- components/shared/Chat/v2/Chat.tsx | 10 ++-- components/shared/Chat/v2/ChatInput.tsx | 9 ++-- components/shared/Chat/v2/ChatMessages.tsx | 20 ++++++-- containers/layout/AppLayout.tsx | 18 +++++--- hooks/useChat.ts | 53 ++++++++++++++++++++++ 5 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 hooks/useChat.ts diff --git a/components/shared/Chat/v2/Chat.tsx b/components/shared/Chat/v2/Chat.tsx index 43f1b329..50edd911 100644 --- a/components/shared/Chat/v2/Chat.tsx +++ b/components/shared/Chat/v2/Chat.tsx @@ -9,18 +9,22 @@ import Icon from "../../Icon"; export type ChatProps = { userId: string; + roomId?: string; lobbyMessages: MessageType[]; friendList: FriendType[]; roomMessages: MessageType[]; maxHeight?: string; + onSubmit: (message: Pick) => void; }; export default function Chat({ userId, + roomId, lobbyMessages, friendList, roomMessages, maxHeight = "calc(100vh - 168px)", + onSubmit, }: Readonly) { const [messages, setMessages] = useState(lobbyMessages); const [target, setTarget] = useState<[ChatTab["id"], string | null]>([ @@ -64,9 +68,9 @@ export default function Chat({ const handleToggleTarget = (id: FriendType["target"]) => setTarget(["friend", id]); - const handleSubmit = (message: MessageType) => { + const handleSubmit = (message: Pick) => { if (activeTab === "friend" && !friendRoom) return; - setMessages((pre) => [...pre, message]); + onSubmit(message); }; return ( @@ -108,7 +112,7 @@ export default function Chat({ )}
diff --git a/components/shared/Chat/v2/ChatInput.tsx b/components/shared/Chat/v2/ChatInput.tsx index 868dafb3..f49bfb44 100644 --- a/components/shared/Chat/v2/ChatInput.tsx +++ b/components/shared/Chat/v2/ChatInput.tsx @@ -4,13 +4,13 @@ import Icon from "../../Icon"; import type { MessageType } from "./ChatMessages"; type ChatInputProps = { - userId: string; + roomId?: string; disabled: boolean; - onSubmit: (message: MessageType) => void; + onSubmit: (message: Pick) => void; }; export default function ChatInput({ - userId, + roomId, disabled, onSubmit, }: Readonly) { @@ -24,8 +24,7 @@ export default function ChatInput({ if (!content) return; setValue(""); onSubmit({ - from: userId, - target: "", + target: roomId ? `ROOM_${roomId}` : "TODO:OTHER", content, }); }; diff --git a/components/shared/Chat/v2/ChatMessages.tsx b/components/shared/Chat/v2/ChatMessages.tsx index e90cf623..bf87f9c2 100644 --- a/components/shared/Chat/v2/ChatMessages.tsx +++ b/components/shared/Chat/v2/ChatMessages.tsx @@ -1,11 +1,21 @@ import { useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; -import Avatar from "../../Avatar"; +import Avatar from "@/components/shared/Avatar"; +import type { UserInfo } from "@/requests/users"; + +type User = Pick; export type MessageType = { - from: string; - target: string; + /** The source of the message. */ + from: "SYSTEM" | User; + /** The content of the user message. */ content: string; + /** The recipient of the message. + * @ "LOBBY" | "ROOM_[:roomId]" - + */ + target: string; + /** The timestamp of the message. */ + timestamp: string; }; type ChatMessageProps = { @@ -31,7 +41,7 @@ export default function ChatMessages({
{messages.map(({ from, content }, index) => { - const isSystem = from === "System"; + const isSystem = from === "SYSTEM"; const isMe = from === userId; const isSameUser = from === messages[index + 1]?.from; @@ -71,7 +81,7 @@ export default function ChatMessages({ isMe && "text-right mt-1" )} > - {isMe ? "我" : from} + {isMe ? "我" : from.nickname}
diff --git a/containers/layout/AppLayout.tsx b/containers/layout/AppLayout.tsx index 4204dfdf..8a54d738 100644 --- a/containers/layout/AppLayout.tsx +++ b/containers/layout/AppLayout.tsx @@ -1,13 +1,17 @@ -import { PropsWithChildren, useReducer } from "react"; +import { PropsWithChildren } from "react"; import Header from "@/components/shared/Header"; import Sidebar from "@/components/shared/Sidebar"; import Chat from "@/components/shared/Chat/v2/Chat"; +import useChat from "@/hooks/useChat"; export default function Layout({ children }: PropsWithChildren) { - const [isChatVisible, toggleChatVisibility] = useReducer( - (preState) => !preState, - false - ); + const { + roomId, + messageList, + isChatVisible, + toggleChatVisibility, + handleSubmitText, + } = useChat(); return (
@@ -21,9 +25,11 @@ export default function Layout({ children }: PropsWithChildren) {
)} diff --git a/hooks/useChat.ts b/hooks/useChat.ts new file mode 100644 index 00000000..8f7f65c6 --- /dev/null +++ b/hooks/useChat.ts @@ -0,0 +1,53 @@ +import { useEffect, useReducer, useState } from "react"; +import type { MessageType } from "@/components/shared/Chat/v2/ChatMessages"; +import useChatroom from "./context/useChatroom"; +import useSocketCore from "./context/useSocketCore"; +import useUser from "./useUser"; + +export default function useChat() { + const { lastMessage, sendChatMessage, joinChatroom, leaveChatroom } = + useChatroom(); + const [isChatVisible, toggleChatVisibility] = useReducer( + (preState: boolean) => !preState, + false + ); + const { socket } = useSocketCore(); + const [messageList, setMessageList] = useState([]); + const { getRoomId } = useUser(); + const roomId = getRoomId(); + + // join chatroom by roomId + useEffect(() => { + if (!roomId) return; + if (!socket || !socket.connected) return; + joinChatroom(roomId); + return () => leaveChatroom(roomId); + }, [joinChatroom, leaveChatroom, roomId, socket, socket?.connected]); + + // update message list while received new message from websocket + useEffect(() => { + if (!lastMessage || !roomId) return; + if (lastMessage.target === `ROOM_${roomId}`) { + setMessageList((prev) => [...prev, lastMessage]); + } + }, [lastMessage, roomId]); + + const handleSubmitText = ( + message: Pick + ) => { + if (!message.content) return; + const data: Pick = { + content: message.content, + target: message.target, + }; + sendChatMessage(data); + }; + + return { + roomId, + messageList, + isChatVisible, + toggleChatVisibility, + handleSubmitText, + }; +} From ac37b361db76b350344598e43a5ededddbce257f Mon Sep 17 00:00:00 2001 From: Johnson Mao Date: Sat, 5 Oct 2024 21:59:09 +0800 Subject: [PATCH 3/3] refactor: chat hook and remove unused component --- .../rooms/RoomChatroom/ChatMessage.test.tsx | 53 ----------- components/rooms/RoomChatroom/ChatMessage.tsx | 25 ----- .../rooms/RoomChatroom/RoomChatroom.test.tsx | 55 ----------- .../rooms/RoomChatroom/RoomChatroom.tsx | 92 ------------------- components/rooms/RoomChatroom/index.ts | 19 ---- containers/provider/ChatroomProvider.tsx | 2 +- hooks/useChat.ts | 14 +-- pages/index.tsx | 2 +- 8 files changed, 10 insertions(+), 252 deletions(-) delete mode 100644 components/rooms/RoomChatroom/ChatMessage.test.tsx delete mode 100644 components/rooms/RoomChatroom/ChatMessage.tsx delete mode 100644 components/rooms/RoomChatroom/RoomChatroom.test.tsx delete mode 100644 components/rooms/RoomChatroom/RoomChatroom.tsx delete mode 100644 components/rooms/RoomChatroom/index.ts diff --git a/components/rooms/RoomChatroom/ChatMessage.test.tsx b/components/rooms/RoomChatroom/ChatMessage.test.tsx deleted file mode 100644 index 61d96a16..00000000 --- a/components/rooms/RoomChatroom/ChatMessage.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import ChatMessage, { ChatMessageProps } from "./ChatMessage"; -import { TEXT_COLORS_CLASS } from "./ChatMessage"; - -const TEST_USER_MESSAGE_PROPS = { - from: { id: "test_user", nickname: "test_nickname" }, - content: "test textContent", - timestamp: new Date().toISOString(), - target: "LOBBY", -}; - -const TEST_SYSTEM_MESSAGE_PROPS: ChatMessageProps = { - from: "SYSTEM", - content: "test textContent", - timestamp: new Date().toISOString(), - target: "LOBBY", -}; - -describe("ChatMessage", () => { - it('should render correct text with the given "content" prop', () => { - render(); - const messageElement = screen.getByText(TEST_USER_MESSAGE_PROPS.content); - - expect(messageElement).toBeInTheDocument(); - }); - - it('should render correct text color when prop "from" is "SYSTEM"', () => { - const { container } = render( - - ); - const rootElement = container.querySelector("div"); - - expect(rootElement).toHaveClass(TEXT_COLORS_CLASS.SYSTEM); - }); - - it('should render correct text color when prop "from" is "USER"', () => { - const { container } = render(); - const rootElement = container.querySelector("div"); - - expect(rootElement).toHaveClass(TEXT_COLORS_CLASS.USER); - }); - - it('should render correct nickname when prop "from" is "USER"', () => { - const { baseElement } = render( - - ); - - expect(baseElement.textContent).toMatch( - TEST_USER_MESSAGE_PROPS.from.nickname - ); - }); -}); diff --git a/components/rooms/RoomChatroom/ChatMessage.tsx b/components/rooms/RoomChatroom/ChatMessage.tsx deleted file mode 100644 index 63a8a601..00000000 --- a/components/rooms/RoomChatroom/ChatMessage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { MessageType } from "."; -import { cn } from "@/lib/utils"; -import React, { JSX, ComponentProps } from "react"; - -export const TEXT_COLORS_CLASS = { - SYSTEM: "text-[#2F88FF]", - USER: "text-[#A8A8A8]", -}; - -function ChatMessage(props: MessageType): JSX.Element { - const textColorClass = - props.from === "SYSTEM" ? TEXT_COLORS_CLASS.SYSTEM : TEXT_COLORS_CLASS.USER; - - const senderText = props.from === "SYSTEM" ? "" : props.from.nickname + ": "; - - return ( -
- {senderText} - {props.content} -
- ); -} -export type ChatMessageProps = ComponentProps; - -export default ChatMessage; diff --git a/components/rooms/RoomChatroom/RoomChatroom.test.tsx b/components/rooms/RoomChatroom/RoomChatroom.test.tsx deleted file mode 100644 index 97840848..00000000 --- a/components/rooms/RoomChatroom/RoomChatroom.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { render, fireEvent } from "@testing-library/react"; -import RoomChatroom from "./RoomChatroom"; - -describe("RoomChatroom", () => { - // mock scrollTo fn - const scrollToMock = jest.fn(); - const originalScrollTo = window.HTMLElement.prototype.scrollTo; - window.HTMLElement.prototype.scrollTo = scrollToMock; - - it("should clean the input value when click submit button", () => { - const { getByRole, getByText } = render(); - - const textarea = getByRole("textarea") as HTMLTextAreaElement; - const submitButton = getByText("發送"); - - fireEvent.change(textarea, { target: { value: "測試輸入" } }); - fireEvent.click(submitButton); - - expect(textarea.value).toBe(""); - }); - - it("should clean the input value when keydown Enter", () => { - const { getByRole } = render(); - const textarea = getByRole("textarea") as HTMLTextAreaElement; - - fireEvent.change(textarea, { target: { value: "ABC" } }); - fireEvent.keyDown(textarea, { code: "Enter" }); - - expect(textarea.value).toBe(""); - }); - - it("shouldn't clean the input value when keydown shift + Enter", () => { - const { getByRole } = render(); - const textarea = getByRole("textarea") as HTMLTextAreaElement; - - fireEvent.change(textarea, { target: { value: "ABC" } }); - fireEvent.keyDown(textarea, { code: "Enter", shiftKey: true }); - - expect(textarea.value).toBe("ABC"); - }); - - it("shouldn't clean the input value when keydown Enter, but key is 'Process' ", () => { - const { getByRole } = render(); - const textarea = getByRole("textarea") as HTMLTextAreaElement; - - fireEvent.change(textarea, { target: { value: "ABC" } }); - fireEvent.keyDown(textarea, { code: "Enter", key: "Process" }); - - expect(textarea.value).toBe("ABC"); - - // reset to origin scrollTo fn - window.HTMLElement.prototype.scrollTo = originalScrollTo; - }); -}); diff --git a/components/rooms/RoomChatroom/RoomChatroom.tsx b/components/rooms/RoomChatroom/RoomChatroom.tsx deleted file mode 100644 index a0698a90..00000000 --- a/components/rooms/RoomChatroom/RoomChatroom.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState, ChangeEvent, KeyboardEvent, useRef, useEffect } from "react"; -import Button from "@/components/shared/Button"; -import ChatMessage from "./ChatMessage"; -import { MessageType } from "."; -import useChatroom from "@/hooks/context/useChatroom"; -import useSocketCore from "@/hooks/context/useSocketCore"; - -type RoomChatroom = { - roomId: string; -}; - -export default function RoomChatroom({ roomId }: RoomChatroom) { - const { lastMessage, sendChatMessage, joinChatroom, leaveChatroom } = - useChatroom(); - const { socket } = useSocketCore(); - const scrollbarRef = useRef(null); - const [inputValue, setInputValue] = useState(""); - const [messageList, setMessageList] = useState([]); - - // join chatroom by roomId - useEffect(() => { - if (!roomId) return; - if (!socket || !socket.connected) return; - joinChatroom(roomId); - return () => leaveChatroom(roomId); - }, [joinChatroom, leaveChatroom, roomId, socket, socket?.connected]); - - // update message list while received new message from websocket - useEffect(() => { - if (!lastMessage || !roomId) return; - if (lastMessage.target === `ROOM_${roomId}`) { - setMessageList((prev) => [...prev, lastMessage]); - } - }, [lastMessage, roomId]); - - // scroll to bottom when messageList updated - useEffect(() => { - scrollbarRef.current?.scrollTo({ - top: scrollbarRef.current.scrollHeight, - }); - }, [messageList]); - - function handleTextChange(event: ChangeEvent) { - setInputValue(event.target.value); - } - - function handleInputKeyDown(event: KeyboardEvent) { - if (!event.shiftKey && event.code === "Enter" && event.key !== "Process") { - event.preventDefault(); - handleSubmitText(); - } - } - - function handleSubmitText() { - if (!inputValue) return; - const data: Pick = { - content: inputValue, - target: `ROOM_${roomId}`, - }; - sendChatMessage(data); - setInputValue(""); - } - return ( -
-
- {messageList.map((msg, index) => ( - - ))} -
-
-