handle more messages

This commit is contained in:
Jake Walker 2022-08-09 14:51:09 +01:00
parent f40ad116fe
commit deb55c2c6d
5 changed files with 89 additions and 33 deletions

View file

@ -1,5 +1,5 @@
import { AppShell, Box, Button, Grid, Group, Input, Stack, TextInput } from "@mantine/core" 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 { useDispatch, useSelector } from "react-redux"
import ChannelList from "./components/ChannelList" import ChannelList from "./components/ChannelList"
import Header from "./components/Header" import Header from "./components/Header"
@ -12,16 +12,23 @@ function App() {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const dispatch = useDispatch(); const dispatch = useDispatch();
const isConnected = useSelector((state: RootState) => state.chat.isConnected);
const currentChannel = useSelector((state: RootState) => state.chat.currentChannel); const currentChannel = useSelector((state: RootState) => state.chat.currentChannel);
const messages = useSelector((state: RootState) => state.chat.messages); const messages = useSelector((state: RootState) => state.chat.messages);
const chatDisabled = currentChannel === null; const messagesEndRef = createRef<HTMLDivElement>();
const chatDisabled = currentChannel === null || !isConnected;
const sendMessage = () => { const sendMessage = () => {
dispatch(chatActions.sendMessage({ content: message })); dispatch(chatActions.sendMessage({ content: message }));
setMessage(""); setMessage("");
} }
useEffect(() => {
messagesEndRef.current?.scrollIntoView();
}, [messages]);
return ( return (
<AppShell <AppShell
padding={0} padding={0}
@ -30,13 +37,14 @@ function App() {
header={<Header opened={opened} setOpened={setOpened} />} header={<Header opened={opened} setOpened={setOpened} />}
aside={<MemberList />} aside={<MemberList />}
> >
<Stack sx={{ height: "calc(100vh - 60px)" }} spacing={0}> <Stack sx={{ height: "calc(100vh - 60px)", maxHeight: "calc(100vh - 60px)" }} spacing={0}>
<Stack sx={{ overflowY: "auto", flexGrow: 1, flexShrink: 0 }} p="md"> <Stack sx={{ overflowY: "scroll", flexGrow: 1 }} p="md">
{messages.map((msg, i) => ( {messages.map((msg, i) => (
<Message key={`msg${i}`} message={msg} /> <Message key={`msg${i}`} message={msg} />
))} ))}
<div ref={messagesEndRef} />
</Stack> </Stack>
<Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`, flexGrow: 0, flexShrink: 0 })}> <Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}` })}>
<TextInput sx={{ flexGrow: 1 }} placeholder="Message" disabled={chatDisabled} value={message} onChange={(event) => setMessage(event.target.value)} onKeyUp={(event) => { <TextInput sx={{ flexGrow: 1 }} placeholder="Message" disabled={chatDisabled} value={message} onChange={(event) => setMessage(event.target.value)} onKeyUp={(event) => {
if (event.key === "Enter") { if (event.key === "Enter") {
sendMessage(); sendMessage();

View file

@ -1,4 +1,6 @@
import { Header as MHeader, Group, Text, MediaQuery, Burger, createStyles } from '@mantine/core'; import { Header as MHeader, Group, Text, MediaQuery, Burger, createStyles } from '@mantine/core';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
interface HeaderProps { interface HeaderProps {
opened: boolean opened: boolean
@ -18,6 +20,7 @@ const useStyles = createStyles((theme) => ({
function Header(props: HeaderProps) { function Header(props: HeaderProps) {
const { classes } = useStyles(); const { classes } = useStyles();
const channelName = useSelector((state: RootState) => state.chat.currentChannel);
return ( return (
<MHeader height={60} p="md" sx={(theme) => ({ <MHeader height={60} p="md" sx={(theme) => ({
@ -34,7 +37,7 @@ function Header(props: HeaderProps) {
</MediaQuery> </MediaQuery>
<Text size="xl" my="auto" className={classes.title}>Echo Web</Text> <Text size="xl" my="auto" className={classes.title}>Echo Web</Text>
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}> <MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
<Text size="md" my="auto">Channel 1</Text> <Text size="md" my="auto">{channelName}</Text>
</MediaQuery> </MediaQuery>
</Group> </Group>
</MHeader> </MHeader>

View file

@ -7,19 +7,16 @@ interface MessageProps {
function Message(props: MessageProps) { function Message(props: MessageProps) {
return ( return (
<Group sx={{ flexWrap: "nowrap" }}> <Stack spacing={0} sx={{ flexGrow: 1, flexShrink: 1 }}>
<Avatar color={props.message.color} radius="xl" sx={{ flexGrow: 0, flexShrink: 0, marginBottom: "auto" }}>LS</Avatar> <Text>
<Stack spacing="xs" sx={{ flexGrow: 1, flexShrink: 1 }}> <Text component="span" color={props.message.color}>{props.message.author}</Text>
<Text> {' '}
{props.message.author} <Text component="span" size="sm" my="auto" color="dimmed">at {props.message.date.toLocaleString()}</Text>
{' '} </Text>
<Text component="span" size="sm" my="auto" color="dimmed">at {props.message.date}</Text> <Text sx={{ whiteSpace: "pre-wrap" }}>
</Text> {props.message.content}
<Text> </Text>
{props.message.content} </Stack>
</Text>
</Stack>
</Group>
) )
} }

View file

@ -22,7 +22,8 @@ enum MessageType {
ResCommandData = "commandData", ResCommandData = "commandData",
ResChannelHistory = "channelHistory", ResChannelHistory = "channelHistory",
ResErrorOccurred = "errorOccured", ResErrorOccurred = "errorOccured",
ResAdditionalHistory = "additionalHistory" ResAdditionalHistory = "additionalHistory",
ResUserlistUpdate = "userlistUpdate"
} }
interface ServerMessage { interface ServerMessage {
@ -37,7 +38,7 @@ export interface ChatMessage {
content: string; content: string;
author: string; author: string;
color: string; color: string;
date: number; date: string;
} }
const defaultMetadata = ['Unknown', '#ffffff', '0']; const defaultMetadata = ['Unknown', '#ffffff', '0'];
@ -84,34 +85,63 @@ const chatMiddleware: Middleware = (store) => {
case MessageType.ResServerData: case MessageType.ResServerData:
store.dispatch(chatActions.updateChannels({ channels: JSON.parse(msg.data[0]) })); store.dispatch(chatActions.updateChannels({ channels: JSON.parse(msg.data[0]) }));
store.dispatch(chatActions.updateMotd({ motd: msg.data[1] })); 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]; return user[0];
})})); })}));
break; break;
case MessageType.ResOutboundMessage: case MessageType.ResOutboundMessage:
let metadata = msg.metadata || defaultMetadata; let outboundMessageMetadata = msg.metadata || defaultMetadata;
store.dispatch(chatActions.recieveMessage({ message: { store.dispatch(chatActions.recieveMessage({ message: {
author: metadata[0], author: outboundMessageMetadata[0],
content: msg.data, content: msg.data,
color: metadata[1], color: outboundMessageMetadata[1],
date: 0 date: (new Date(parseInt(outboundMessageMetadata[2]) * 1000)).toLocaleString()
}})) }}))
break; break;
case MessageType.ResCommandData: case MessageType.ResCommandData:
metadata = msg.metadata || defaultMetadata; let commandDataMetadata = msg.metadata || defaultMetadata;
store.dispatch(chatActions.recieveMessage({ message: { store.dispatch(chatActions.recieveMessage({ message: {
author: metadata[0], author: commandDataMetadata[0],
content: msg.data, content: msg.data.join("\n"),
color: metadata[1], color: commandDataMetadata[1],
date: 0 date: (new Date(parseInt(commandDataMetadata[2]) * 1000)).toLocaleString()
}})); }}));
break; 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: default:
store.dispatch(chatActions.recieveMessage({ message: { store.dispatch(chatActions.recieveMessage({ message: {
author: 'Server', author: 'Server',
content: JSON.stringify(msg), content: JSON.stringify(msg),
color: '#ff0000', color: '#ff0000',
date: 0 date: (new Date()).toLocaleString()
}})); }}));
} }
} }

View file

@ -56,17 +56,35 @@ const chatSlice = createSlice({
}>) => { }>) => {
state.motd = action.payload.motd; state.motd = action.payload.motd;
}, },
updateUsers: (state, action: PayloadAction<{ setUsers: (state, action: PayloadAction<{
users: string[] users: string[]
}>) => { }>) => {
state.users = action.payload.users; 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<{ changeChannel: (state, action: PayloadAction<{
channel: string channel: string
}>) => { }>) => {
state.currentChannel = action.payload.channel; state.currentChannel = action.payload.channel;
state.messages = []; state.messages = [];
}, },
addMessages: (state, action: PayloadAction<{
messages: ChatMessage[]
}>) => {
state.messages.push(...action.payload.messages);
},
sendMessage: (state, action: PayloadAction<{ sendMessage: (state, action: PayloadAction<{
content: string content: string
}>) => {} }>) => {}