diff --git a/client/src/components/ChatMenu.tsx b/client/src/components/ChatMenu.tsx
new file mode 100644
index 0000000..a971b01
--- /dev/null
+++ b/client/src/components/ChatMenu.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { useChatState } from "../contexts/ChatContext";
+
+function ChatMenu() {
+ const { actions, state } = useChatState();
+
+ return (
+
+
+
+
+ );
+}
+
+export default React.memo(ChatMenu);
diff --git a/client/src/components/ChatProvider.tsx b/client/src/components/ChatProvider.tsx
index 4f70a99..18bd036 100644
--- a/client/src/components/ChatProvider.tsx
+++ b/client/src/components/ChatProvider.tsx
@@ -1,6 +1,6 @@
import React, { useReducer } from "react";
import { User } from "../../../shared";
-import { ChatContext, Message } from "../contexts/ChatContext";
+import { ChatContext, ChatState, Message } from "../contexts/ChatContext";
import { chatReducer } from "../reducers/chatReducers";
export function ChatProvider({ children }: { children: React.ReactNode }) {
@@ -8,6 +8,8 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
messages: [],
currentUser: null,
isConnected: false,
+ participants: [],
+ selectedMenu: "chat",
});
const value = {
@@ -23,6 +25,10 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
setUser: (user: User) => dispatch({ type: "SET_USER", payload: user }),
setConnected: (isConnected: boolean) =>
dispatch({ type: "SET_CONNECTED", payload: isConnected }),
+ setParticipants: (participants: User[]) =>
+ dispatch({ type: "SET_PARTICIPANTS", payload: participants }),
+ setMenu: (menu: ChatState["selectedMenu"]) =>
+ dispatch({ type: "SET_MENU", payload: menu }),
},
};
diff --git a/client/src/components/MessageDisplay.tsx b/client/src/components/MessageDisplay.tsx
index e40b397..4748fb0 100644
--- a/client/src/components/MessageDisplay.tsx
+++ b/client/src/components/MessageDisplay.tsx
@@ -17,6 +17,9 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
const userId = state.currentUser?.userId;
const messagesEndRef = useRef(null); // ref to an empty div at the end to scroll to
+ // this counter is used to generate unique keys for join/leave messages. This is not ideal and with more time I'd introduce server-side message IDs for all messages, not just chat messages
+ let messageCounter = 0;
+
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
@@ -65,7 +68,7 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
return (
{message.payload.user.name} joined!
@@ -74,7 +77,7 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
return (
{message.payload.user.name} left!
diff --git a/client/src/components/ParticipantsList.tsx b/client/src/components/ParticipantsList.tsx
new file mode 100644
index 0000000..a221cb6
--- /dev/null
+++ b/client/src/components/ParticipantsList.tsx
@@ -0,0 +1,18 @@
+import { useChatState } from "../contexts/ChatContext";
+
+function ParticipantList() {
+ const { state } = useChatState();
+
+ return (
+ <>
+ Participants
+
+ {state.participants.map((participant) => (
+ - {participant.name}
+ ))}
+
+ >
+ );
+}
+
+export default ParticipantList;
diff --git a/client/src/components/RoomHeader.tsx b/client/src/components/RoomHeader.tsx
new file mode 100644
index 0000000..6c0495f
--- /dev/null
+++ b/client/src/components/RoomHeader.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { useChatState } from "../contexts/ChatContext";
+
+interface RoomHeaderProps {
+ roomId: string;
+ isConnected: boolean;
+}
+
+function RoomHeader(props: RoomHeaderProps) {
+ const { state } = useChatState();
+
+ return (
+
+ Room ID: {props.roomId}
+ {state.currentUser && (
+ User: {state.currentUser?.name}
+ )}
+
+ {props.isConnected ? "Connected" : "Not Connected"}
+
+
+ );
+}
+
+export default React.memo(RoomHeader);
diff --git a/client/src/contexts/ChatContext.tsx b/client/src/contexts/ChatContext.tsx
index 0be8839..651b97c 100644
--- a/client/src/contexts/ChatContext.tsx
+++ b/client/src/contexts/ChatContext.tsx
@@ -15,6 +15,8 @@ export type ChatState = {
messages: Message[];
currentUser: User | null;
isConnected: boolean;
+ participants: User[];
+ selectedMenu: "chat" | "participants";
};
export type ChatAction =
@@ -23,7 +25,9 @@ export type ChatAction =
| { type: "DELETE_MESSAGE"; payload: string }
| { type: "SET_USER"; payload: User }
| { type: "SET_CONNECTED"; payload: boolean }
- | { type: "CLEAR_MESSAGES" };
+ | { type: "CLEAR_MESSAGES" }
+ | { type: "SET_PARTICIPANTS"; payload: User[] }
+ | { type: "SET_MENU"; payload: ChatState["selectedMenu"] };
export type ChatActions = {
editMessage: (messageId: string, newContent: string) => void;
@@ -31,6 +35,8 @@ export type ChatActions = {
deleteMessage: (messageId: string) => void;
setUser: (user: User) => void;
setConnected: (isConnected: boolean) => void;
+ setParticipants: (participants: User[]) => void;
+ setMenu: (menu: ChatState["selectedMenu"]) => void;
};
interface ChatContextType {
diff --git a/client/src/hooks/useWebSocket.ts b/client/src/hooks/useWebSocket.ts
index edd1f64..edaade9 100644
--- a/client/src/hooks/useWebSocket.ts
+++ b/client/src/hooks/useWebSocket.ts
@@ -39,6 +39,10 @@ export function useWebSocket(roomId: string) {
);
break;
+ case "USER_LIST":
+ actions.setParticipants(message.payload.users);
+ break;
+
default:
console.error("Unknown message type", message);
}
diff --git a/client/src/pages/Room.tsx b/client/src/pages/Room.tsx
index 162ca40..1219425 100644
--- a/client/src/pages/Room.tsx
+++ b/client/src/pages/Room.tsx
@@ -5,38 +5,41 @@ import { useWebSocket } from "../hooks/useWebSocket";
import ChatMessages from "../components/MessageDisplay";
import ChatInput from "../components/ChatInput";
import { useChatState } from "../contexts/ChatContext";
+import ParticipantList from "../components/ParticipantsList";
+import ChatMenu from "../components/ChatMenu";
+import RoomHeader from "../components/RoomHeader";
function Room() {
- const { roomId } = useParams();
+ const { roomId } = useParams(); // can never be null as this Room component is only ever rendered if react-router matches the route WITH a roomId
const { isConnected, connect, sendMessage } = useWebSocket(roomId ?? "");
const { state } = useChatState();
+ if (!isConnected) {
+ return (
+
+
+
+ );
+ }
+
+ // is connected
return (
-
- Room ID: {roomId}
- {state.currentUser && (
- User: {state.currentUser?.name}
- )}
-
- {isConnected ? "Connected" : "Not Connected"}
-
-
+
- {!isConnected && }
- {isConnected && (
+
+ {state.selectedMenu === "chat" && (
<>
>
)}
+
+ {state.selectedMenu === "participants" && }
);
diff --git a/client/src/reducers/chatReducers.ts b/client/src/reducers/chatReducers.ts
index ad81f20..aa04d6b 100644
--- a/client/src/reducers/chatReducers.ts
+++ b/client/src/reducers/chatReducers.ts
@@ -52,6 +52,17 @@ export function chatReducer(state: ChatState, action: ChatAction): ChatState {
...state,
messages: [],
};
+ case "SET_PARTICIPANTS":
+ return {
+ ...state,
+ participants: action.payload,
+ };
+
+ case "SET_MENU":
+ return {
+ ...state,
+ selectedMenu: action.payload,
+ };
default:
return state;
}