diff --git a/src/App.tsx b/src/App.tsx index 0932852..9542d36 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() - if (!username) { - const newName = prompt("Username:") as string - setUsername(newName) - } - return ( -
-

Local Area Network Chat Application

-
This web application was built for the purposes of an EPQ project.
- - { } -
- ) -} -export default App; \ No newline at end of file + const [username, setUsername] = useState(); + const [messages, setMessages] = useState([]); + if (!username) { + const newName = prompt("Username:") as string; + setUsername(newName); + } + return ( +
+

Local Area Network Chat Application

+
+				This web application was built for the purposes of an EPQ
+				project.
+			
+ {messages.map((message) => { + return ; + })} + { + + } +
+ ); +}; +export default App; diff --git a/src/Chat/Chat.css b/src/Chat/Chat.css index bee333e..c5b74e3 100644 --- a/src/Chat/Chat.css +++ b/src/Chat/Chat.css @@ -3,7 +3,7 @@ overflow-y: auto; overflow-wrap: normal; display: flex; - flex-direction: column-reverse; + flex-direction: column; } .entry-box { diff --git a/src/Chat/Chat.tsx b/src/Chat/Chat.tsx deleted file mode 100644 index c2e92ba..0000000 --- a/src/Chat/Chat.tsx +++ /dev/null @@ -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 = - 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 ( -
-
-
-
-
- -
- ) -} -export default ChatWrapper; \ No newline at end of file diff --git a/src/Chat/ChatWrapper.tsx b/src/Chat/ChatWrapper.tsx new file mode 100644 index 0000000..66e0301 --- /dev/null +++ b/src/Chat/ChatWrapper.tsx @@ -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([]); + 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([ + , + ]); + }); + 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 ( +
+
+ {messages} +
+ + + + +
+ ); +}; +export default ChatWrapper; diff --git a/src/Chat/server.ts b/src/Chat/server.ts new file mode 100644 index 0000000..d155270 --- /dev/null +++ b/src/Chat/server.ts @@ -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; +} \ No newline at end of file