diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 41c050d..4834fa6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { AppShell, Box, Button, Grid, Group, Input, Stack, TextInput } from "@mantine/core" -import { useState } from "react" +import { useState, createRef, useEffect } from "react" import { useDispatch, useSelector } from "react-redux" import ChannelList from "./components/ChannelList" import Header from "./components/Header" @@ -12,16 +12,23 @@ function App() { const [opened, setOpened] = useState(false); const [message, setMessage] = useState(""); const dispatch = useDispatch(); + const isConnected = useSelector((state: RootState) => state.chat.isConnected); const currentChannel = useSelector((state: RootState) => state.chat.currentChannel); const messages = useSelector((state: RootState) => state.chat.messages); - const chatDisabled = currentChannel === null; + const messagesEndRef = createRef(); + + const chatDisabled = currentChannel === null || !isConnected; const sendMessage = () => { dispatch(chatActions.sendMessage({ content: message })); setMessage(""); } + useEffect(() => { + messagesEndRef.current?.scrollIntoView(); + }, [messages]); + return ( } aside={} > - - + + {messages.map((msg, i) => ( ))} +
- ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`, flexGrow: 0, flexShrink: 0 })}> + ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}` })}> setMessage(event.target.value)} onKeyUp={(event) => { if (event.key === "Enter") { sendMessage(); diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 8db6028..25787dc 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,4 +1,6 @@ import { Header as MHeader, Group, Text, MediaQuery, Burger, createStyles } from '@mantine/core'; +import { useSelector } from 'react-redux'; +import { RootState } from '../store'; interface HeaderProps { opened: boolean @@ -18,6 +20,7 @@ const useStyles = createStyles((theme) => ({ function Header(props: HeaderProps) { const { classes } = useStyles(); + const channelName = useSelector((state: RootState) => state.chat.currentChannel); return ( ({ @@ -34,7 +37,7 @@ function Header(props: HeaderProps) { Echo Web - Channel 1 + {channelName} diff --git a/frontend/src/components/Message.tsx b/frontend/src/components/Message.tsx index a60a15e..37bcc00 100644 --- a/frontend/src/components/Message.tsx +++ b/frontend/src/components/Message.tsx @@ -7,19 +7,16 @@ interface MessageProps { function Message(props: MessageProps) { return ( - - LS - - - {props.message.author} - {' '} - at {props.message.date} - - - {props.message.content} - - - + + + {props.message.author} + {' '} + at {props.message.date.toLocaleString()} + + + {props.message.content} + + ) } diff --git a/frontend/src/middleware/chat.tsx b/frontend/src/middleware/chat.tsx index 32772b9..539a920 100644 --- a/frontend/src/middleware/chat.tsx +++ b/frontend/src/middleware/chat.tsx @@ -22,7 +22,8 @@ enum MessageType { ResCommandData = "commandData", ResChannelHistory = "channelHistory", ResErrorOccurred = "errorOccured", - ResAdditionalHistory = "additionalHistory" + ResAdditionalHistory = "additionalHistory", + ResUserlistUpdate = "userlistUpdate" } interface ServerMessage { @@ -37,7 +38,7 @@ export interface ChatMessage { content: string; author: string; color: string; - date: number; + date: string; } const defaultMetadata = ['Unknown', '#ffffff', '0']; @@ -84,34 +85,63 @@ const chatMiddleware: Middleware = (store) => { case MessageType.ResServerData: store.dispatch(chatActions.updateChannels({ channels: JSON.parse(msg.data[0]) })); store.dispatch(chatActions.updateMotd({ motd: msg.data[1] })); - store.dispatch(chatActions.updateUsers({ users: JSON.parse(msg.data[2]).map((user) => { + store.dispatch(chatActions.setUsers({ users: JSON.parse(msg.data[2]).map((user: any) => { return user[0]; })})); break; case MessageType.ResOutboundMessage: - let metadata = msg.metadata || defaultMetadata; + let outboundMessageMetadata = msg.metadata || defaultMetadata; store.dispatch(chatActions.recieveMessage({ message: { - author: metadata[0], + author: outboundMessageMetadata[0], content: msg.data, - color: metadata[1], - date: 0 + color: outboundMessageMetadata[1], + date: (new Date(parseInt(outboundMessageMetadata[2]) * 1000)).toLocaleString() }})) break; case MessageType.ResCommandData: - metadata = msg.metadata || defaultMetadata; + let commandDataMetadata = msg.metadata || defaultMetadata; store.dispatch(chatActions.recieveMessage({ message: { - author: metadata[0], - content: msg.data, - color: metadata[1], - date: 0 + author: commandDataMetadata[0], + content: msg.data.join("\n"), + color: commandDataMetadata[1], + date: (new Date(parseInt(commandDataMetadata[2]) * 1000)).toLocaleString() }})); break; + case MessageType.ResChannelHistory: + store.dispatch(chatActions.addMessages({ + messages: msg.data.map((message: any) => ({ + author: message[0], + content: message[3], + color: message[4], + date: (new Date(parseInt(message[5]) * 1000)).toLocaleString() + })) + })) + break; + case MessageType.ResChannelUpdate: + const [user, fromChannel, toChannel] = msg.data; + store.dispatch(chatActions.updateUsers({ + user, + fromChannel, + toChannel + })); + break; + case MessageType.ResConnectionTerminated: + store.dispatch(chatActions.recieveMessage({ message: { + author: "Server", + content: msg.data, + color: '#ff0000', + date: (new Date()).toLocaleString() + }})); + socket?.close(); + break; + case MessageType.ResUserlistUpdate: + break; default: store.dispatch(chatActions.recieveMessage({ message: { author: 'Server', content: JSON.stringify(msg), color: '#ff0000', - date: 0 + date: (new Date()).toLocaleString() }})); } } diff --git a/frontend/src/slices/chat.tsx b/frontend/src/slices/chat.tsx index f46d762..e122324 100644 --- a/frontend/src/slices/chat.tsx +++ b/frontend/src/slices/chat.tsx @@ -56,17 +56,35 @@ const chatSlice = createSlice({ }>) => { state.motd = action.payload.motd; }, - updateUsers: (state, action: PayloadAction<{ + setUsers: (state, action: PayloadAction<{ users: string[] }>) => { state.users = action.payload.users; }, + updateUsers: (state, action: PayloadAction<{ + user: string, + fromChannel: string, + toChannel: string + }>) => { + if (action.payload.toChannel === state.currentChannel) { + if (!state.users.includes(action.payload.user)) { + state.users.push(action.payload.user); + } + } else { + state.users = state.users.filter((user) => user !== action.payload.user); + } + }, changeChannel: (state, action: PayloadAction<{ channel: string }>) => { state.currentChannel = action.payload.channel; state.messages = []; }, + addMessages: (state, action: PayloadAction<{ + messages: ChatMessage[] + }>) => { + state.messages.push(...action.payload.messages); + }, sendMessage: (state, action: PayloadAction<{ content: string }>) => {}