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:
|
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 { useState } from "react"
|
||||||
|
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"
|
||||||
import MemberList from "./components/MemberList"
|
import MemberList from "./components/MemberList"
|
||||||
import Message from "./components/Message"
|
import Message from "./components/Message"
|
||||||
|
import { chatActions } from "./slices/chat"
|
||||||
|
import { RootState } from "./store"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [opened, setOpened] = useState(false);
|
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 (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
|
@ -16,15 +30,19 @@ function App() {
|
||||||
header={<Header opened={opened} setOpened={setOpened} />}
|
header={<Header opened={opened} setOpened={setOpened} />}
|
||||||
aside={<MemberList />}
|
aside={<MemberList />}
|
||||||
>
|
>
|
||||||
<Stack sx={{ maxHeight: "calc(100vh - 60px)" }} spacing={0}>
|
<Stack sx={{ height: "calc(100vh - 60px)" }} spacing={0}>
|
||||||
<Stack sx={{ overflowY: "auto", flexGrow: 1 }} p="md">
|
<Stack sx={{ overflowY: "auto", flexGrow: 1, flexShrink: 0 }} p="md">
|
||||||
{[...Array(10)].map((e, i) => (
|
{messages.map((msg, i) => (
|
||||||
<Message />
|
<Message key={`msg${i}`} message={msg} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`})}>
|
<Group p="md" sx={(theme) => ({ borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]}`, flexGrow: 0, flexShrink: 0 })}>
|
||||||
<Input sx={{ flexGrow: 1 }} placeholder="Message" />
|
<TextInput sx={{ flexGrow: 1 }} placeholder="Message" disabled={chatDisabled} value={message} onChange={(event) => setMessage(event.target.value)} onKeyUp={(event) => {
|
||||||
<Button>Send</Button>
|
if (event.key === "Enter") {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
|
<Button disabled={chatDisabled} onClick={() => sendMessage()}>Send</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</AppShell>
|
</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 { 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 }) {
|
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 (
|
return (
|
||||||
<Navbar width={{ base: '100%', sm: 200, md: 300 }} p="sm" hidden={props.hidden} hiddenBreakpoint="sm">
|
<Navbar width={{ base: '100%', sm: 200, md: 300 }} p="sm" hidden={props.hidden} hiddenBreakpoint="sm">
|
||||||
<Stack sx={{ flexGrow: 1 }}>
|
<Stack sx={{ flexGrow: 1 }}>
|
||||||
<Box>
|
<Box>
|
||||||
{[...Array(4)].map((e, i) => (
|
{channels.map((channel) => (
|
||||||
<NavLink label={`Channel ${i+1}`} icon={<Hash />}/>
|
<NavLink
|
||||||
|
key={`channel_${channel}`}
|
||||||
|
label={channel}
|
||||||
|
icon={<Hash />}
|
||||||
|
active={channel === currentChannel}
|
||||||
|
onClick={() => dispatch(chatActions.changeChannel({ channel }))}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button.Group>
|
<Button.Group>
|
||||||
<Button sx={{ flexGrow: 1 }}>Connect</Button>
|
<Button
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
onClick={connectClick}
|
||||||
|
loading={!isConnected && isConnecting}
|
||||||
|
>
|
||||||
|
{connectText}
|
||||||
|
</Button>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Settings size={20} />
|
<Settings size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { Aside, MediaQuery, Navbar, NavLink } from "@mantine/core"
|
import { Aside, MediaQuery, Navbar, NavLink } from "@mantine/core"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
import { RootState } from "../store"
|
||||||
|
|
||||||
function MemberList() {
|
function MemberList() {
|
||||||
|
const users = useSelector((state: RootState) => state.chat.users);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
|
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
|
||||||
<Aside p="sm" hiddenBreakpoint="sm" width={{ sm: 200, lg: 300 }}>
|
<Aside p="sm" hiddenBreakpoint="sm" width={{ sm: 200, lg: 300 }}>
|
||||||
{[...Array(4)].map((e, i) => (
|
{users.map((user) => (
|
||||||
<NavLink label={`Person ${i+1}`}/>
|
<NavLink key={`user${user}`} label={user}/>
|
||||||
))}
|
))}
|
||||||
</Aside>
|
</Aside>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import { Avatar, Box, Group, Stack, Text } from "@mantine/core"
|
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 (
|
return (
|
||||||
<Group sx={{ flexWrap: "nowrap" }}>
|
<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 }}>
|
<Stack spacing="xs" sx={{ flexGrow: 1, flexShrink: 1 }}>
|
||||||
<Text>
|
<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>
|
||||||
<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>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { MantineProvider } from '@mantine/core';
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import store from './store';
|
import { store } from './store';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
@ -1,24 +1,147 @@
|
||||||
import { Middleware } from "@reduxjs/toolkit";
|
import { Middleware } from "@reduxjs/toolkit";
|
||||||
import WebSocket from 'ws';
|
import chat, { chatActions } from "../slices/chat";
|
||||||
import { chatActions } from "../slices/chat";
|
|
||||||
|
|
||||||
const chatMiddleware: Middleware = (store) => (next) => (action) => {
|
enum MessageType {
|
||||||
if (!chatActions.startConnecting.match(action)) {
|
ReqServerInfo = "serverInfoRequest",
|
||||||
return next(action);
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = new WebSocket("ws://127.0.0.1:4000?server=195.201.123.169:16000&username=jake");
|
interface ServerMessage {
|
||||||
|
userid?: string
|
||||||
|
messagetype: MessageType;
|
||||||
|
subtype?: string;
|
||||||
|
data: any;
|
||||||
|
metadata?: string;
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('connect', () => {
|
export interface ChatMessage {
|
||||||
console.log('Connected to WebSocket!');
|
content: string;
|
||||||
store.dispatch(chatActions.connectionEstablished());
|
author: string;
|
||||||
});
|
color: string;
|
||||||
|
date: number;
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('message', (message: any) => {
|
const defaultMetadata = ['Unknown', '#ffffff', '0'];
|
||||||
store.dispatch(chatActions.recieveMessage({ message }))
|
|
||||||
});
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
const msg: ServerMessage = {
|
||||||
|
userid: data.userid || '0',
|
||||||
|
messagetype: data.messagetype,
|
||||||
|
subtype: data.subtype,
|
||||||
|
data: "",
|
||||||
|
metadata: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
msg.data = JSON.parse(data.data);
|
||||||
|
} catch (SyntaxError) {
|
||||||
|
msg.data = data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
next(action);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default chatMiddleware;
|
export default chatMiddleware;
|
||||||
|
|
|
@ -1,36 +1,78 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { ChatMessage } from "../middleware/chat";
|
||||||
|
|
||||||
export interface ChatState {
|
export interface ChatState {
|
||||||
messages: any[];
|
messages: ChatMessage[];
|
||||||
|
users: string[];
|
||||||
|
channels: string[];
|
||||||
|
motd: string | null;
|
||||||
isEstablishingConnection: boolean;
|
isEstablishingConnection: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
currentChannel: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ChatState = {
|
const initialState: ChatState = {
|
||||||
messages: [],
|
messages: [],
|
||||||
|
users: [],
|
||||||
|
channels: [],
|
||||||
|
motd: null,
|
||||||
isEstablishingConnection: false,
|
isEstablishingConnection: false,
|
||||||
isConnected: false
|
isConnected: false,
|
||||||
|
currentChannel: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatSlice = createSlice({
|
const chatSlice = createSlice({
|
||||||
name: "chat",
|
name: "chat",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
startConnecting: (state) => {
|
connect: (state) => {
|
||||||
|
state.isConnected = false;
|
||||||
state.isEstablishingConnection = true;
|
state.isEstablishingConnection = true;
|
||||||
},
|
},
|
||||||
connectionEstablished: (state) => {
|
connected: (state) => {
|
||||||
state.isConnected = true;
|
state.isConnected = true;
|
||||||
state.isEstablishingConnection = true;
|
state.isEstablishingConnection = true;
|
||||||
},
|
},
|
||||||
|
disconnect: (state) => {
|
||||||
|
state.isConnected = false;
|
||||||
|
state.isEstablishingConnection = true;
|
||||||
|
},
|
||||||
|
disconnected: (state) => {
|
||||||
|
state.isConnected = false;
|
||||||
|
state.isEstablishingConnection = false;
|
||||||
|
},
|
||||||
recieveMessage: (state, action: PayloadAction<{
|
recieveMessage: (state, action: PayloadAction<{
|
||||||
message: any
|
message: ChatMessage
|
||||||
}>) => {
|
}>) => {
|
||||||
state.messages.push(action.payload.message);
|
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 const chatActions = chatSlice.actions;
|
||||||
|
|
||||||
export default chatSlice;
|
export default chatSlice.reducer;
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import chatMiddleware from './middleware/chat';
|
||||||
|
import chatSlice from './slices/chat';
|
||||||
|
|
||||||
export default configureStore({
|
export const store = configureStore({
|
||||||
reducer: {}
|
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