Solved issue previously ignored by statically rendering components through

unrelated bug  Enter keypress event fires the value presence checker incorrectly
This commit allows further functionality to be expanded to the application such as proper registration support and complex user types
This commit is contained in:
Zhongheng Liu 2024-01-14 14:05:50 +02:00
commit 847ecd9a69
No known key found for this signature in database
5 changed files with 193 additions and 135 deletions

View file

@ -1,19 +1,31 @@
import React, { useState } from "react";
import ChatWrapper from "./Chat/Chat";
import ChatWrapper from "./Chat/ChatWrapper";
import "./App.css";
import { Message } from "./Chat/types";
import { MessageContainer } from "./Chat/MessageContainer";
const App = (): React.ReactElement => {
const [username, setUsername] = useState<string>()
if (!username) {
const newName = prompt("Username:") as string
setUsername(newName)
}
return (
<div className="App">
<h1>Local Area Network Chat Application</h1>
<pre>This web application was built for the purposes of an EPQ project.</pre>
{<ChatWrapper user={username as string} brokerURL=""/> }
</div>
)
}
export default App;
const [username, setUsername] = useState<string>();
const [messages, setMessages] = useState<Message[]>([]);
if (!username) {
const newName = prompt("Username:") as string;
setUsername(newName);
}
return (
<div className="App">
<h1>Local Area Network Chat Application</h1>
<pre>
This web application was built for the purposes of an EPQ
project.
</pre>
{messages.map((message) => {
return <MessageContainer {...message} />;
})}
{
<ChatWrapper
user={username as string}
/>
}
</div>
);
};
export default App;

View file

@ -3,7 +3,7 @@
overflow-y: auto;
overflow-wrap: normal;
display: flex;
flex-direction: column-reverse;
flex-direction: column;
}
.entry-box {

View file

@ -1,118 +0,0 @@
import React, { useEffect, useState } from "react";
import { MessageContainer } from "./MessageContainer";
import { Client, Stomp, StompHeaders } from "@stomp/stompjs";
import { Message, MessageType } from "./types";
import { renderToStaticMarkup } from 'react-dom/server';
import './Chat.css';
// The last bit of magic sauce to make this work
// EXPLANATION
//
const domain = window.location.hostname
const port = "8080"
const connectionAddress = `ws://${domain}:${port}/ws`
const endpoints = {
destination: "/app/chat",
subscription: "/sub/chat",
history: "/api/v1/chat/history/"
}
const ChatWrapper = (
{
user,
brokerURL,
}:
{
user: string,
brokerURL: string,
}
): React.ReactElement => {
const stompClient = new Client({
brokerURL: connectionAddress
})
// TODO solve issue with non-static markup
stompClient.onConnect = (frame) => {
stompClient.subscribe(endpoints.subscription, (message) => {
console.log(`Collected new message: ${message.body}`);
const messageBody = JSON.parse(message.body) as Message
// if (messageBody.type !== MessageType.MESSAGE) {return;}
const messageElement = <MessageContainer {...messageBody} />
console.log(messageElement);
// Temporary solution
// The solution lacks interactibility - because it's static markup
const container = document.getElementById("chat-inner") as HTMLDivElement
// Truly horrible and disgusting
container.innerHTML += renderToStaticMarkup(messageElement)
});
stompClient.publish({
body: JSON.stringify({
type: MessageType.HELLO,
fromUserId: user,
toUserId: "everyone",
content: `${user} has joined the server!`,
timeMillis: Date.now()
}),
destination: endpoints.destination
})
}
// Generic error handlers
stompClient.onWebSocketError = (error) => {
console.error('Error with websocket', error);
};
stompClient.onStompError = (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
// Button press event handler.
const sendData = () => {
console.log("WebSockets handler invoked.")
// There must be a react-native and non-document-getElementById way to do this
// TODO Explore
const entryElement: HTMLInputElement = document.getElementById("data-entry") as HTMLInputElement
if (!entryElement.value) {alert("Message cannot be empty!"); return;}
const messageData: Message =
{
type: MessageType.MESSAGE,
fromUserId: user,
toUserId: "everyone",
content: entryElement.value,
timeMillis: Date.now()
}
console.log(`STOMP connection status: ${stompClient.connected}`);
stompClient.publish({
body: JSON.stringify(messageData),
destination: endpoints.destination,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
entryElement.value = "";
}
useEffect(() => {
// Stomp client is disconnected after each re-render
// This should be actively avoided
stompClient.activate()
return () => {
stompClient.deactivate()
}
}, [stompClient])
// https://www.w3schools.com/jsref/obj_keyboardevent.asp
document.addEventListener("keypress", (ev: KeyboardEvent) => {
if (ev.key == "Enter") {
sendData();
}
})
return (
<div className="chat">
<div className="chat-inner-wrapper">
<div id="chat-inner">
</div>
</div>
<span className="entry-box"><input id="data-entry"></input><button onClick={() => sendData()}>Send</button></span>
</div>
)
}
export default ChatWrapper;

120
src/Chat/ChatWrapper.tsx Normal file
View file

@ -0,0 +1,120 @@
import React, { ReactElement, useEffect, useRef, useState } from "react";
import { MessageContainer } from "./MessageContainer";
import { Client, Stomp, StompHeaders } from "@stomp/stompjs";
import { Message, MessageType } from "./types";
import { renderToStaticMarkup } from "react-dom/server";
import "./Chat.css";
// The last bit of magic sauce to make this work
// EXPLANATION
//
const domain = window.location.hostname;
const port = "8080";
const connectionAddress = `ws://${domain}:${port}/ws`;
const endpoints = {
destination: "/app/chat",
subscription: "/sub/chat",
history: "/api/v1/msg/",
};
const ChatWrapper = ({ user }: { user: string }): React.ReactElement => {
const [messages, setMessages] = useState<ReactElement[]>([]);
let stompClientRef = useRef(
new Client({
brokerURL: connectionAddress,
})
);
// TODO solve issue with non-static markup
stompClientRef.current.onConnect = (frame) => {
stompClientRef.current.subscribe(endpoints.subscription, (message) => {
console.log(`Collected new message: ${message.body}`);
const messageBody = JSON.parse(message.body) as Message;
console.log(messageBody);
setMessages((message) => {
return message.concat([
<MessageContainer
key={`${messageBody.type}@${messageBody.timeMillis}`}
{...messageBody}
/>,
]);
});
console.log(messages);
});
stompClientRef.current.publish({
body: JSON.stringify({
type: MessageType.HELLO,
fromUserId: user,
toUserId: "everyone",
content: `${user} has joined the server!`,
timeMillis: Date.now(),
}),
destination: endpoints.destination,
});
};
// Generic error handlers
stompClientRef.current.onWebSocketError = (error) => {
console.error("Error with websocket", error);
};
stompClientRef.current.onStompError = (frame) => {
console.error("Broker reported error: " + frame.headers["message"]);
console.error("Additional details: " + frame.body);
};
// Button press event handler.
const sendData = () => {
console.log("WebSockets handler invoked.");
// There must be a react-native and non-document-getElementById way to do this
// TODO Explore
const entryElement: HTMLInputElement = document.getElementById(
"data-entry"
) as HTMLInputElement;
if (!entryElement.value) {
alert("Message cannot be empty!");
return;
}
const messageData: Message = {
type: MessageType.MESSAGE,
fromUserId: user,
toUserId: "everyone",
content: entryElement.value,
timeMillis: Date.now(),
};
console.log(
`STOMP connection status: ${stompClientRef.current.connected}`
);
stompClientRef.current.publish({
body: JSON.stringify(messageData),
destination: endpoints.destination,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
entryElement.value = "";
};
useEffect(() => {
// Stomp client is disconnected after each re-render
// This should be actively avoided
stompClientRef.current.activate();
return () => {
stompClientRef.current.deactivate();
};
}, [stompClientRef]);
// https://www.w3schools.com/jsref/obj_keyboardevent.asp
document.addEventListener("keypress", (ev: KeyboardEvent) => {
if (ev.key == "Enter") {
sendData();
}
});
return (
<div className="chat">
<div className="chat-inner-wrapper">
{messages}
</div>
<span className="entry-box">
<input id="data-entry"></input>
<button onClick={() => sendData()}>Send</button>
</span>
</div>
);
};
export default ChatWrapper;

44
src/Chat/server.ts Normal file
View file

@ -0,0 +1,44 @@
import { Client } from "@stomp/stompjs";
import { Message, MessageType } from "./types";
const domain = window.location.hostname
const port = "8080"
const connectionAddress = `ws://${domain}:${port}/ws`
const endpoints = {
destination: "/app/chat",
subscription: "/sub/chat",
history: "/api/v1/msg/"
}
export const createStompConnection = (user: string, subUpdateHandler: (message: Message) => void) => {
const stompClient = new Client({
brokerURL: connectionAddress
})
stompClient.onConnect = (frame) => {
stompClient.subscribe(endpoints.subscription, (message) => {
console.log(`Collected new message: ${message.body}`);
const messageBody = JSON.parse(message.body) as Message
console.log(messageBody);
subUpdateHandler(messageBody);
});
stompClient.publish({
body: JSON.stringify({
type: MessageType.HELLO,
fromUserId: user,
toUserId: "everyone",
content: `${user} has joined the server!`,
timeMillis: Date.now()
}),
destination: endpoints.destination
})
}
// Generic error handlers
stompClient.onWebSocketError = (error) => {
console.error('Error with websocket', error);
};
stompClient.onStompError = (frame) => {
console.error('Broker reported error: ' + frame.headers['message']);
console.error('Additional details: ' + frame.body);
};
return stompClient;
}