Finish basic chat
This commit is contained in:
parent
5a53e7069d
commit
f40ad116fe
9 changed files with 281 additions and 48 deletions
|
@ -1,2 +1,2 @@
|
|||
allowedServers:
|
||||
- "195.201.123.169:16000"
|
||||
- "127.0.0.1:16000"
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
import { AppShell, Box, Button, Grid, Group, Input, Stack } from "@mantine/core"
|
||||
import { AppShell, Box, Button, Grid, Group, Input, Stack, TextInput } from "@mantine/core"
|
||||
import { useState } from "react"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import ChannelList from "./components/ChannelList"
|
||||
import Header from "./components/Header"
|
||||
import MemberList from "./components/MemberList"
|
||||
import Message from "./components/Message"
|
||||
import { chatActions } from "./slices/chat"
|
||||
import { RootState } from "./store"
|
||||
|
||||
function App() {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
const currentChannel = useSelector((state: RootState) => state.chat.currentChannel);
|
||||
const messages = useSelector((state: RootState) => state.chat.messages);
|
||||
|
||||
const chatDisabled = currentChannel === null;
|
||||
|
||||
const sendMessage = () => {
|
||||
dispatch(chatActions.sendMessage({ content: message }));
|
||||
setMessage("");
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
|
@ -16,15 +30,19 @@ function App() {
|
|||
header={<Header opened={opened} setOpened={setOpened} />}
|
||||
aside={<MemberList />}
|
||||
>
|
||||
<Stack sx={{ maxHeight: "calc(100vh - 60px)" }} spacing={0}>
|
||||
<Stack sx={{ overflowY: "auto", flexGrow: 1 }} p="md">
|
||||
{[...Array(10)].map((e, i) => (
|
||||
<Message />
|
||||
<Stack sx={{ height: "calc(100vh - 60px)" }} spacing={0}>
|
||||
<Stack sx={{ overflowY: "auto", flexGrow: 1, flexShrink: 0 }} p="md">
|
||||
{messages.map((msg, i) => (
|
||||
<Message key={`msg${i}`} message={msg} />
|
||||
))}
|
||||
</Stack>
|
||||
<Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`})}>
|
||||
<Input sx={{ flexGrow: 1 }} placeholder="Message" />
|
||||
<Button>Send</Button>
|
||||
<Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`, flexGrow: 0, flexShrink: 0 })}>
|
||||
<TextInput sx={{ flexGrow: 1 }} placeholder="Message" disabled={chatDisabled} value={message} onChange={(event) => setMessage(event.target.value)} onKeyUp={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
sendMessage();
|
||||
}
|
||||
}}/>
|
||||
<Button disabled={chatDisabled} onClick={() => sendMessage()}>Send</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</AppShell>
|
||||
|
|
|
@ -1,18 +1,49 @@
|
|||
import { ActionIcon, Box, Button, Group, Navbar, NavLink, Stack } from "@mantine/core";
|
||||
import { Box, Button, Navbar, NavLink, Stack } from "@mantine/core";
|
||||
import { Root } from "react-dom/client";
|
||||
import { Hash, Settings } from "react-feather";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { chatActions } from "../slices/chat";
|
||||
import { RootState } from "../store";
|
||||
|
||||
function ChannelList(props: { hidden: boolean }) {
|
||||
const dispatch = useDispatch();
|
||||
const isConnected = useSelector((state: RootState) => state.chat.isConnected);
|
||||
const isConnecting = useSelector((state: RootState) => state.chat.isEstablishingConnection);
|
||||
const channels = useSelector((state: RootState) => state.chat.channels);
|
||||
const currentChannel = useSelector((state: RootState) => state.chat.currentChannel);
|
||||
|
||||
const connectText = isConnected ? 'Disconnect' : 'Connect';
|
||||
const connectClick = () => {
|
||||
if (isConnected) {
|
||||
dispatch(chatActions.disconnect());
|
||||
} else {
|
||||
dispatch(chatActions.connect());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar width={{ base: '100%', sm: 200, md: 300 }} p="sm" hidden={props.hidden} hiddenBreakpoint="sm">
|
||||
<Stack sx={{ flexGrow: 1 }}>
|
||||
<Box>
|
||||
{[...Array(4)].map((e, i) => (
|
||||
<NavLink label={`Channel ${i+1}`} icon={<Hash />}/>
|
||||
{channels.map((channel) => (
|
||||
<NavLink
|
||||
key={`channel_${channel}`}
|
||||
label={channel}
|
||||
icon={<Hash />}
|
||||
active={channel === currentChannel}
|
||||
onClick={() => dispatch(chatActions.changeChannel({ channel }))}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Button.Group>
|
||||
<Button sx={{ flexGrow: 1 }}>Connect</Button>
|
||||
<Button
|
||||
sx={{ flexGrow: 1 }}
|
||||
onClick={connectClick}
|
||||
loading={!isConnected && isConnecting}
|
||||
>
|
||||
{connectText}
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Settings size={20} />
|
||||
</Button>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { Aside, MediaQuery, Navbar, NavLink } from "@mantine/core"
|
||||
import { useSelector } from "react-redux"
|
||||
import { RootState } from "../store"
|
||||
|
||||
function MemberList() {
|
||||
const users = useSelector((state: RootState) => state.chat.users);
|
||||
|
||||
return (
|
||||
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
|
||||
<Aside p="sm" hiddenBreakpoint="sm" width={{ sm: 200, lg: 300 }}>
|
||||
{[...Array(4)].map((e, i) => (
|
||||
<NavLink label={`Person ${i+1}`}/>
|
||||
{users.map((user) => (
|
||||
<NavLink key={`user${user}`} label={user}/>
|
||||
))}
|
||||
</Aside>
|
||||
</MediaQuery>
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { Avatar, Box, Group, Stack, Text } from "@mantine/core"
|
||||
import { ChatMessage } from "../middleware/chat"
|
||||
|
||||
function Message() {
|
||||
interface MessageProps {
|
||||
message: ChatMessage
|
||||
}
|
||||
|
||||
function Message(props: MessageProps) {
|
||||
return (
|
||||
<Group sx={{ flexWrap: "nowrap" }}>
|
||||
<Avatar color="cyan" radius="xl" sx={{ flexGrow: 0, flexShrink: 0, marginBottom: "auto" }}>LS</Avatar>
|
||||
<Avatar color={props.message.color} radius="xl" sx={{ flexGrow: 0, flexShrink: 0, marginBottom: "auto" }}>LS</Avatar>
|
||||
<Stack spacing="xs" sx={{ flexGrow: 1, flexShrink: 1 }}>
|
||||
<Text>
|
||||
Author
|
||||
{props.message.author}
|
||||
{' '}
|
||||
<Text component="span" size="sm" my="auto" color="dimmed">at 4:13pm</Text>
|
||||
<Text component="span" size="sm" my="auto" color="dimmed">at {props.message.date}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus nemo nisi saepe quasi eaque laudantium autem aliquid eligendi, nihil aut id sunt iusto nulla. Modi ut provident obcaecati quibusdam veritatis?
|
||||
{props.message.content}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { MantineProvider } from '@mantine/core';
|
|||
import App from './App'
|
||||
import './index.css';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store';
|
||||
import { store } from './store';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
@ -1,24 +1,147 @@
|
|||
import { Middleware } from "@reduxjs/toolkit";
|
||||
import WebSocket from 'ws';
|
||||
import { chatActions } from "../slices/chat";
|
||||
import chat, { chatActions } from "../slices/chat";
|
||||
|
||||
const chatMiddleware: Middleware = (store) => (next) => (action) => {
|
||||
if (!chatActions.startConnecting.match(action)) {
|
||||
return next(action);
|
||||
enum MessageType {
|
||||
ReqServerInfo = "serverInfoRequest",
|
||||
ReqClientSecret = "clientSecret",
|
||||
ReqConnection = "connectionRequest",
|
||||
ReqDisconnect = "disconnect",
|
||||
ReqInfo = "requestInfo",
|
||||
ReqUserMessage = "userMessage",
|
||||
ReqChangeChannel = "changeChannel",
|
||||
ReqHistory = "historyRequest",
|
||||
ReqLeaveChannel = "leaveChannel",
|
||||
|
||||
ResServerData = "serverData",
|
||||
ResClientSecret = "gotSecret",
|
||||
ResConnectionAccepted = "CRAccepted",
|
||||
ResConnectionDenied = "CRDenied",
|
||||
ResOutboundMessage = "outboundMessage",
|
||||
ResConnectionTerminated = "connectionTerminated",
|
||||
ResChannelUpdate = "channelUpdate",
|
||||
ResCommandData = "commandData",
|
||||
ResChannelHistory = "channelHistory",
|
||||
ResErrorOccurred = "errorOccured",
|
||||
ResAdditionalHistory = "additionalHistory"
|
||||
}
|
||||
|
||||
interface ServerMessage {
|
||||
userid?: string
|
||||
messagetype: MessageType;
|
||||
subtype?: string;
|
||||
data: any;
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string;
|
||||
author: string;
|
||||
color: string;
|
||||
date: number;
|
||||
}
|
||||
|
||||
const defaultMetadata = ['Unknown', '#ffffff', '0'];
|
||||
|
||||
const chatMiddleware: Middleware = (store) => {
|
||||
let socket: WebSocket | null = null;
|
||||
|
||||
return (next) => (action) => {
|
||||
if (chatActions.connect.match(action) && socket === null) {
|
||||
socket = new WebSocket("ws://127.0.0.1:4000?server=127.0.0.1:16000&username=jake&password=password");
|
||||
|
||||
socket.onopen = () => {
|
||||
store.dispatch(chatActions.connected());
|
||||
socket?.send(JSON.stringify({
|
||||
messagetype: MessageType.ReqInfo
|
||||
}));
|
||||
}
|
||||
|
||||
const socket = new WebSocket("ws://127.0.0.1:4000?server=195.201.123.169:16000&username=jake");
|
||||
socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const msg: ServerMessage = {
|
||||
userid: data.userid || '0',
|
||||
messagetype: data.messagetype,
|
||||
subtype: data.subtype,
|
||||
data: "",
|
||||
metadata: "",
|
||||
};
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to WebSocket!');
|
||||
store.dispatch(chatActions.connectionEstablished());
|
||||
});
|
||||
try {
|
||||
msg.data = JSON.parse(data.data);
|
||||
} catch (SyntaxError) {
|
||||
msg.data = data.data;
|
||||
}
|
||||
|
||||
socket.on('message', (message: any) => {
|
||||
store.dispatch(chatActions.recieveMessage({ message }))
|
||||
});
|
||||
try {
|
||||
msg.metadata = JSON.parse(data.metadata);
|
||||
} catch (SyntaxError) {
|
||||
msg.metadata = data.metadata;
|
||||
}
|
||||
|
||||
console.log(msg);
|
||||
|
||||
switch (msg.messagetype) {
|
||||
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) => {
|
||||
return user[0];
|
||||
})}));
|
||||
break;
|
||||
case MessageType.ResOutboundMessage:
|
||||
let metadata = msg.metadata || defaultMetadata;
|
||||
store.dispatch(chatActions.recieveMessage({ message: {
|
||||
author: metadata[0],
|
||||
content: msg.data,
|
||||
color: metadata[1],
|
||||
date: 0
|
||||
}}))
|
||||
break;
|
||||
case MessageType.ResCommandData:
|
||||
metadata = msg.metadata || defaultMetadata;
|
||||
store.dispatch(chatActions.recieveMessage({ message: {
|
||||
author: metadata[0],
|
||||
content: msg.data,
|
||||
color: metadata[1],
|
||||
date: 0
|
||||
}}));
|
||||
break;
|
||||
default:
|
||||
store.dispatch(chatActions.recieveMessage({ message: {
|
||||
author: 'Server',
|
||||
content: JSON.stringify(msg),
|
||||
color: '#ff0000',
|
||||
date: 0
|
||||
}}));
|
||||
}
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
store.dispatch(chatActions.disconnected());
|
||||
}
|
||||
|
||||
socket.onerror = (event) => {
|
||||
console.error('ws error', event);
|
||||
}
|
||||
} else if (chatActions.disconnect.match(action) && socket !== null) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
} else if (chatActions.changeChannel.match(action) && socket !== null) {
|
||||
const changeChannelMessage: ServerMessage = {
|
||||
messagetype: MessageType.ReqChangeChannel,
|
||||
data: action.payload.channel
|
||||
}
|
||||
socket.send(JSON.stringify(changeChannelMessage));
|
||||
} else if (chatActions.sendMessage.match(action) && socket !== null) {
|
||||
const sendMessage: ServerMessage = {
|
||||
messagetype: MessageType.ReqUserMessage,
|
||||
data: action.payload.content
|
||||
}
|
||||
socket.send(JSON.stringify(sendMessage));
|
||||
}
|
||||
|
||||
next(action);
|
||||
}
|
||||
};
|
||||
|
||||
export default chatMiddleware;
|
||||
|
|
|
@ -1,36 +1,78 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ChatMessage } from "../middleware/chat";
|
||||
|
||||
export interface ChatState {
|
||||
messages: any[];
|
||||
messages: ChatMessage[];
|
||||
users: string[];
|
||||
channels: string[];
|
||||
motd: string | null;
|
||||
isEstablishingConnection: boolean;
|
||||
isConnected: boolean;
|
||||
currentChannel: string | null;
|
||||
}
|
||||
|
||||
const initialState: ChatState = {
|
||||
messages: [],
|
||||
users: [],
|
||||
channels: [],
|
||||
motd: null,
|
||||
isEstablishingConnection: false,
|
||||
isConnected: false
|
||||
isConnected: false,
|
||||
currentChannel: null
|
||||
};
|
||||
|
||||
const chatSlice = createSlice({
|
||||
name: "chat",
|
||||
initialState,
|
||||
reducers: {
|
||||
startConnecting: (state) => {
|
||||
connect: (state) => {
|
||||
state.isConnected = false;
|
||||
state.isEstablishingConnection = true;
|
||||
},
|
||||
connectionEstablished: (state) => {
|
||||
connected: (state) => {
|
||||
state.isConnected = true;
|
||||
state.isEstablishingConnection = true;
|
||||
},
|
||||
disconnect: (state) => {
|
||||
state.isConnected = false;
|
||||
state.isEstablishingConnection = true;
|
||||
},
|
||||
disconnected: (state) => {
|
||||
state.isConnected = false;
|
||||
state.isEstablishingConnection = false;
|
||||
},
|
||||
recieveMessage: (state, action: PayloadAction<{
|
||||
message: any
|
||||
message: ChatMessage
|
||||
}>) => {
|
||||
state.messages.push(action.payload.message);
|
||||
}
|
||||
},
|
||||
updateChannels: (state, action: PayloadAction<{
|
||||
channels: string[]
|
||||
}>) => {
|
||||
state.channels = action.payload.channels;
|
||||
},
|
||||
updateMotd: (state, action: PayloadAction<{
|
||||
motd: string
|
||||
}>) => {
|
||||
state.motd = action.payload.motd;
|
||||
},
|
||||
updateUsers: (state, action: PayloadAction<{
|
||||
users: string[]
|
||||
}>) => {
|
||||
state.users = action.payload.users;
|
||||
},
|
||||
changeChannel: (state, action: PayloadAction<{
|
||||
channel: string
|
||||
}>) => {
|
||||
state.currentChannel = action.payload.channel;
|
||||
state.messages = [];
|
||||
},
|
||||
sendMessage: (state, action: PayloadAction<{
|
||||
content: string
|
||||
}>) => {}
|
||||
}
|
||||
});
|
||||
|
||||
export const chatActions = chatSlice.actions;
|
||||
|
||||
export default chatSlice;
|
||||
export default chatSlice.reducer;
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import chatMiddleware from './middleware/chat';
|
||||
import chatSlice from './slices/chat';
|
||||
|
||||
export default configureStore({
|
||||
reducer: {}
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
chat: chatSlice
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
return getDefaultMiddleware().concat([chatMiddleware])
|
||||
}
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
|
Loading…
Reference in a new issue