Builds with TypeScript enabled
Signed-off-by: James Ketrenos <james@ketrenos.com>
This commit is contained in:
parent
57eef0e9cf
commit
29de35b17f
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
logs/*
|
logs/*
|
||||||
|
db/*
|
||||||
|
20
client/package-lock.json
generated
20
client/package-lock.json
generated
@ -12,10 +12,10 @@
|
|||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@mui/icons-material": "^5.11.11",
|
"@mui/icons-material": "^5.11.11",
|
||||||
"@mui/material": "^5.11.15",
|
"@mui/material": "^5.11.15",
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/node": "^18.11.10",
|
"@types/node": "^18.15.11",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.31",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.11",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
@ -30,7 +30,8 @@
|
|||||||
"react-router-dom": "^6.4.4",
|
"react-router-dom": "^6.4.4",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
"react-spinners": "^0.13.6"
|
"react-spinners": "^0.13.6",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@ -16414,16 +16415,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=12.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import './App.css';
|
|
||||||
import { PlayerList } from './PlayerList';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<PlayerList/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
355
client/src/App.tsx
Normal file
355
client/src/App.tsx
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useParams,
|
||||||
|
useNavigate
|
||||||
|
} from "react-router-dom";
|
||||||
|
|
||||||
|
import { GlobalContext, GlobalData } from "./GlobalContext";
|
||||||
|
import { PersonList } from "./PersonList";
|
||||||
|
import { Chat } from "./Chat";
|
||||||
|
|
||||||
|
import "./App.css";
|
||||||
|
import equal from "fast-deep-equal";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const base = process.env.REACT_APP_CHAT_BASE;
|
||||||
|
|
||||||
|
const Table = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const [chatId, setChatId] = useState(params.chatId ? params.chatId : undefined);
|
||||||
|
const [ws, setWs] = useState <WebSocket | undefined>(); /* tracks full websocket lifetime */
|
||||||
|
const [connection, setConnection] = useState<WebSocket|undefined>(undefined); /* set after ws is in OPEN */
|
||||||
|
const [retryConnection, setRetryConnection] = useState(true); /* set when connection should be re-established */
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [color, setColor] = useState(undefined);
|
||||||
|
const [priv, setPriv] = useState(undefined);
|
||||||
|
const [global, setGlobal] = useState<GlobalData>({
|
||||||
|
chatId: undefined,
|
||||||
|
ws: undefined,
|
||||||
|
name: '',
|
||||||
|
chat: []
|
||||||
|
});
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [warning, setWarning] = useState<string>('');
|
||||||
|
|
||||||
|
const fields = ['id', 'state', 'color', 'name', 'private' ];
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onWsOpen = (event: any) => {
|
||||||
|
console.log(`ws: open`);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
/* We do not set the socket as connected until the 'open' message
|
||||||
|
* comes through */
|
||||||
|
setConnection(ws);
|
||||||
|
|
||||||
|
/* Request a full chat-update
|
||||||
|
* We only need chatId and name for App, however in the event
|
||||||
|
* of a network disconnect, we need to refresh the entire chat
|
||||||
|
* state on reload so all bound components reflect the latest
|
||||||
|
* state */
|
||||||
|
event.target.send(JSON.stringify({
|
||||||
|
type: 'chat-update'
|
||||||
|
}));
|
||||||
|
event.target.send(JSON.stringify({
|
||||||
|
type: 'get',
|
||||||
|
fields
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWsMessage = (event: any) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'error':
|
||||||
|
console.error(`App - error`, data.error);
|
||||||
|
setError(data.error);
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
console.warn(`App - warning`, data.warning);
|
||||||
|
setWarning(data.warning);
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`todo: stack warnings in a window and have them disappear one at a time.`);
|
||||||
|
console.log(`app - clearing warning`);
|
||||||
|
setWarning("");
|
||||||
|
}, 3000);
|
||||||
|
break;
|
||||||
|
case 'chat-update':
|
||||||
|
if (!loaded) {
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
console.log(`app - message - ${data.type}`, data.update);
|
||||||
|
|
||||||
|
if ('private' in data.update && !equal(priv, data.update.private)) {
|
||||||
|
const priv = data.update.private;
|
||||||
|
if (priv.name !== name) {
|
||||||
|
console.log(`App - setting name (via private): ${priv.name}`);
|
||||||
|
setName(priv.name);
|
||||||
|
}
|
||||||
|
if (priv.color !== color) {
|
||||||
|
console.log(`App - setting color (via private): ${priv.color}`);
|
||||||
|
setColor(priv.color);
|
||||||
|
}
|
||||||
|
setPriv(priv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('name' in data.update) {
|
||||||
|
if (data.update.name) {
|
||||||
|
console.log(`App - setting name: ${data.update.name}`);
|
||||||
|
setName(data.update.name);
|
||||||
|
} else {
|
||||||
|
setWarning("");
|
||||||
|
setError("");
|
||||||
|
setPriv(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('id' in data.update && data.update.id !== chatId) {
|
||||||
|
console.log(`App - setting chatId ${data.update.id}`);
|
||||||
|
setChatId(data.update.id);
|
||||||
|
}
|
||||||
|
if ('color' in data.update && data.update.color !== color) {
|
||||||
|
console.log(`App - setting color: ${color}`);
|
||||||
|
setColor(data.update.color);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendUpdate = (update: string) => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify(update));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cbResetConnection = useCallback(() => {
|
||||||
|
let timer: any = 0;
|
||||||
|
function reset() {
|
||||||
|
timer = 0;
|
||||||
|
setRetryConnection(true);
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(reset, 5000);
|
||||||
|
};
|
||||||
|
}, [setRetryConnection]);
|
||||||
|
|
||||||
|
const resetConnection = cbResetConnection();
|
||||||
|
|
||||||
|
if (global.ws !== connection
|
||||||
|
|| global.name !== name
|
||||||
|
|| global.chatId !== chatId) {
|
||||||
|
console.log(`board - (app) - setting global`, global, {
|
||||||
|
ws: connection,
|
||||||
|
name,
|
||||||
|
chatId
|
||||||
|
});
|
||||||
|
setGlobal({
|
||||||
|
ws: connection,
|
||||||
|
name,
|
||||||
|
chatId,
|
||||||
|
chat: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWsError = (event: any) => {
|
||||||
|
console.error(`ws: error`, event);
|
||||||
|
const error = `Connection to Ketr Ketran chat server failed! ` +
|
||||||
|
`Connection attempt will be retried every 5 seconds.`;
|
||||||
|
setError(error);
|
||||||
|
setGlobal(Object.assign({}, global, { ws: undefined }));
|
||||||
|
setWs(undefined); /* clear the socket */
|
||||||
|
setConnection(undefined); /* clear the connection */
|
||||||
|
resetConnection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWsClose = (event: any) => {
|
||||||
|
const error = `Connection to Ketr Ketran chat was lost. ` +
|
||||||
|
`Attempting to reconnect...`;
|
||||||
|
console.warn(`ws: close`);
|
||||||
|
setError(error);
|
||||||
|
setGlobal(Object.assign({}, global, { ws: undefined }));
|
||||||
|
setWs(undefined); /* clear the socket */
|
||||||
|
setConnection(undefined); /* clear the connection */
|
||||||
|
resetConnection();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* callback refs are used to provide correct state reference
|
||||||
|
* in the callback handlers, while also preventing rebinding
|
||||||
|
* of event handlers on every render */
|
||||||
|
const refWsOpen = useRef(onWsOpen);
|
||||||
|
useEffect(() => { refWsOpen.current = onWsOpen; });
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
const refWsClose = useRef(onWsClose);
|
||||||
|
useEffect(() => { refWsClose.current = onWsClose; });
|
||||||
|
const refWsError = useRef(onWsError);
|
||||||
|
useEffect(() => { refWsError.current = onWsError; });
|
||||||
|
|
||||||
|
/* This effect is responsible for triggering a new chat load if a
|
||||||
|
* chat id is not provided in the URL. If the chat is provided
|
||||||
|
* in the URL, the backend will create a new chat if necessary
|
||||||
|
* during the WebSocket connection sequence.
|
||||||
|
*
|
||||||
|
* This should be the only HTTP request made from the chat.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatId) {
|
||||||
|
console.log(`Chat in use ${chatId}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Requesting new chat.`);
|
||||||
|
|
||||||
|
window.fetch(`${base}/api/v1/chats/`, {
|
||||||
|
method: 'POST',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status >= 400) {
|
||||||
|
const error = `Unable to connect to Ketr Ketran chat server! ` +
|
||||||
|
`Try refreshing your browser in a few seconds.`;
|
||||||
|
console.error(error);
|
||||||
|
setError(error);
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}).then((update) => {
|
||||||
|
if (update.id !== chatId) {
|
||||||
|
console.log(`Chat available: ${update.id}`);
|
||||||
|
navigate(`${base}/${update.id}`);
|
||||||
|
setChatId(update.id);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}, [chatId, setChatId]);
|
||||||
|
|
||||||
|
/* Once a chat id is known, create the sole WebSocket connection
|
||||||
|
* to the backend. This WebSocket is then shared with any component
|
||||||
|
* that performs chat state updates. Those components should
|
||||||
|
* bind to the 'message:chat-update' WebSocket event and parse
|
||||||
|
* their update information from those messages
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbind = () => {
|
||||||
|
console.log(`table - unbind`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`table - bind`);
|
||||||
|
|
||||||
|
if (!ws && !connection && retryConnection) {
|
||||||
|
let loc = window.location, new_uri;
|
||||||
|
if (loc.protocol === "https:") {
|
||||||
|
new_uri = "wss";
|
||||||
|
} else {
|
||||||
|
new_uri = "ws";
|
||||||
|
}
|
||||||
|
new_uri = `${new_uri}://${loc.host}${base}/api/v1/chats/ws/${chatId}?${count}`;
|
||||||
|
console.log(`Attempting WebSocket connection to ${new_uri}`);
|
||||||
|
setWs(new WebSocket(new_uri));
|
||||||
|
setConnection(undefined);
|
||||||
|
setRetryConnection(false);
|
||||||
|
setCount(count + 1);
|
||||||
|
return unbind;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws) {
|
||||||
|
return unbind;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cbOpen = (e: any) => refWsOpen.current(e);
|
||||||
|
const cbMessage = (e: any) => refWsMessage.current(e);
|
||||||
|
const cbClose = (e: any) => refWsClose.current(e);
|
||||||
|
const cbError = (e: any) => refWsError.current(e);
|
||||||
|
|
||||||
|
ws.addEventListener('open', cbOpen);
|
||||||
|
ws.addEventListener('close', cbClose);
|
||||||
|
ws.addEventListener('error', cbError);
|
||||||
|
ws.addEventListener('message', cbMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unbind();
|
||||||
|
ws.removeEventListener('open', cbOpen);
|
||||||
|
ws.removeEventListener('close', cbClose);
|
||||||
|
ws.removeEventListener('error', cbError);
|
||||||
|
ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [ws, setWs, connection, setConnection,
|
||||||
|
retryConnection, setRetryConnection, chatId,
|
||||||
|
refWsOpen, refWsMessage, refWsClose, refWsError, count, setCount
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`board - (app) - Render with ws: ${ws ? '!' : ''}NULL, connection: ${connection ? '!' : ''}NULL`);
|
||||||
|
|
||||||
|
return <GlobalContext.Provider value={global}>
|
||||||
|
{ /* <PingPong/> */}
|
||||||
|
<div className="Table">
|
||||||
|
<div className="Sidebar">
|
||||||
|
{name !== "" && <PersonList />}
|
||||||
|
{name !== "" && <Chat />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GlobalContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [personId, setPersonId] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (personId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.fetch(`${base}/api/v1/chats/`, {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'same-origin', /* include cookies */
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status >= 400) {
|
||||||
|
const error = `Unable to connect to Ketr Ketran chat server! ` +
|
||||||
|
`Try refreshing your browser in a few seconds.`;
|
||||||
|
console.error(error);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
console.log(res.headers);
|
||||||
|
return res.json();
|
||||||
|
}).then((data) => {
|
||||||
|
setPersonId(data.person);
|
||||||
|
}).catch((error) => {
|
||||||
|
});
|
||||||
|
}, [personId, setPersonId]);
|
||||||
|
|
||||||
|
if (!personId) {
|
||||||
|
return <>{error}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<Table />} path={`${base}/:chatId`} />
|
||||||
|
<Route element={<Table />} path={`${base}`} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
115
client/src/Chat.css
Normal file
115
client/src/Chat.css
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
.Chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.25rem 0.25rem 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList {
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
/* for Firefox */
|
||||||
|
min-height: 0;
|
||||||
|
/*scroll-behavior: smooth;*/
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .System {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatInput {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .System.MuiListItem-gutters {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
max-width: calc(100% - 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .System .MuiTypography-body1 {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .MuiListItem-gutters {
|
||||||
|
padding: 2px 0 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .MuiTypography-body1 {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .MuiTypography-body1 > div {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .System .MuiTypography-body1 {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .MuiTypography-body2 {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .MuiListItemText-multiline {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 4px 0px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .PersonColor {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .Resource {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
pointer-events: none;
|
||||||
|
margin: 0 0.125rem;
|
||||||
|
background-size: 130%;
|
||||||
|
border: 2px solid #444;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
top: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .Resource > div {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.625rem;
|
||||||
|
right: -0.625rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid white;
|
||||||
|
background-color: rgb(36, 148, 46);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatList .Dice {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
}
|
190
client/src/Chat.tsx
Normal file
190
client/src/Chat.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import React, { useState, useEffect, useContext, useRef, useCallback, useMemo } from "react";
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Moment from 'react-moment';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import 'moment-timezone';
|
||||||
|
import equal from "fast-deep-equal";
|
||||||
|
|
||||||
|
import "./Chat.css";
|
||||||
|
import { PersonColor } from './PersonColor';
|
||||||
|
import { GlobalContext } from "./GlobalContext";
|
||||||
|
|
||||||
|
export type ChatData = {
|
||||||
|
date: string,
|
||||||
|
message: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const Chat = () => {
|
||||||
|
const [lastTop, setLastTop] = useState(0);
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const [latest, setLatest] = useState('');
|
||||||
|
const [scrollTime, setScrollTime] = useState(0);
|
||||||
|
const [chat, setChat] = useState<ChatData[]>([]);
|
||||||
|
const [startTime, setStartTime] = useState(0);
|
||||||
|
|
||||||
|
const { ws, name } = useContext(GlobalContext);
|
||||||
|
const fields = useMemo(() => [
|
||||||
|
'chat', 'startTime'
|
||||||
|
], []);
|
||||||
|
const onWsMessage = (event: any) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'game-update':
|
||||||
|
console.log(`chat - game update`);
|
||||||
|
if (data.update.chat && !equal(data.update.chat, chat)) {
|
||||||
|
console.log(`chat - game update - ${data.update.chat.length} lines`);
|
||||||
|
setChat(data.update.chat);
|
||||||
|
}
|
||||||
|
if (data.update.startTime && data.update.startTime !== startTime) {
|
||||||
|
setStartTime(data.update.startTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
useEffect(() => { refWsMessage.current = onWsMessage; });
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ws) { return; }
|
||||||
|
const cbMessage = (e: any) => refWsMessage.current(e);
|
||||||
|
ws.addEventListener('message', cbMessage);
|
||||||
|
return () => {
|
||||||
|
ws.removeEventListener('message', cbMessage);
|
||||||
|
}
|
||||||
|
}, [ws, refWsMessage]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ws) { return; }
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'get',
|
||||||
|
fields
|
||||||
|
}));
|
||||||
|
}, [ws, fields]);
|
||||||
|
|
||||||
|
const chatKeyPress = useCallback((event: any) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
if (!autoScroll) {
|
||||||
|
setAutoScroll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({ type: 'chat', message: event.target.value }));
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
}, [ws, setAutoScroll, autoScroll]);
|
||||||
|
|
||||||
|
const chatScroll = (event: any) => {
|
||||||
|
const chatList = event.target,
|
||||||
|
fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop));
|
||||||
|
|
||||||
|
/* If scroll is within 20 pixels of the bottom, turn on auto-scroll */
|
||||||
|
const shouldAutoscroll = (fromBottom < 20);
|
||||||
|
|
||||||
|
if (shouldAutoscroll !== autoScroll) {
|
||||||
|
setAutoScroll(shouldAutoscroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the list should not auto scroll, then cache the current
|
||||||
|
* top of the list and record when we did this so we honor
|
||||||
|
* the auto-scroll for at least 500ms */
|
||||||
|
if (!shouldAutoscroll) {
|
||||||
|
const target = Math.round(chatList.scrollTop);
|
||||||
|
if (target !== lastTop) {
|
||||||
|
setLastTop(target);
|
||||||
|
setScrollTime(Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const chatList = document.getElementById("ChatList");
|
||||||
|
if (!chatList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentTop = Math.round(chatList.scrollTop);
|
||||||
|
|
||||||
|
if (autoScroll) {
|
||||||
|
/* Auto-scroll to the bottom of the chat window */
|
||||||
|
const target = Math.round(chatList.scrollHeight - chatList.offsetHeight);
|
||||||
|
if (currentTop !== target) {
|
||||||
|
chatList.scrollTop = target;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Maintain current position in scrolled view if the user hasn't
|
||||||
|
* been scrolling in the past 0.5s */
|
||||||
|
if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) {
|
||||||
|
chatList.scrollTop = lastTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages = chat.map((item: any, index) => {
|
||||||
|
let message;
|
||||||
|
/* Do not perform extra parsing on player-generated
|
||||||
|
* messages */
|
||||||
|
if (item.normalChat) {
|
||||||
|
message = <div key={`line-${index}`}>{item.message}</div>;
|
||||||
|
} else {
|
||||||
|
const punctuation = item.message.match(/(\.+$)/);
|
||||||
|
let period: any;
|
||||||
|
if (punctuation) {
|
||||||
|
period = punctuation[1];
|
||||||
|
} else {
|
||||||
|
period = '';
|
||||||
|
}
|
||||||
|
let lines = item.message.split('.');
|
||||||
|
message = lines
|
||||||
|
.filter((line: string) => line.trim() !== '')
|
||||||
|
.map((line: string, index: number) => {
|
||||||
|
let start = line, message;
|
||||||
|
while (start) {
|
||||||
|
message = <>{start}{message}</>;
|
||||||
|
start = '';
|
||||||
|
}
|
||||||
|
return <div key={`line-${index}`}>{ message }{ period }</div>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem key={`msg-${item.date}-${index}`}
|
||||||
|
className={item.color ? '' : 'System'}>
|
||||||
|
{ item.color &&
|
||||||
|
<PersonColor color={item.color}/>
|
||||||
|
}
|
||||||
|
<ListItemText primary={message}
|
||||||
|
secondary={item.color && <Moment fromNow trim date={item.date > Date.now() ?
|
||||||
|
Date.now() : item.date} interval={1000}/>} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chat.length && chat[chat.length - 1].date !== latest) {
|
||||||
|
setLatest(chat[chat.length - 1].date);
|
||||||
|
setAutoScroll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className="Chat">
|
||||||
|
<List className="ChatList" id="ChatList" onScroll={chatScroll}>
|
||||||
|
{ messages }
|
||||||
|
</List>
|
||||||
|
<TextField className="ChatInput"
|
||||||
|
disabled={!name}
|
||||||
|
onKeyPress={chatKeyPress}
|
||||||
|
label={startTime !== 0 && <>Game duration: <Moment tz={"Etc/GMT"}
|
||||||
|
format="h:mm:ss"
|
||||||
|
trim
|
||||||
|
durationFromNow interval={1000}
|
||||||
|
date={startTime}/></>}
|
||||||
|
variant="outlined"/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Chat };
|
@ -1,6 +1,12 @@
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
const global = {
|
export type GlobalData = {
|
||||||
|
chatId: string | undefined,
|
||||||
|
ws: WebSocket | undefined,
|
||||||
|
name: string,
|
||||||
|
chat: string[]
|
||||||
|
}
|
||||||
|
const global: GlobalData = {
|
||||||
chatId: undefined,
|
chatId: undefined,
|
||||||
ws: undefined,
|
ws: undefined,
|
||||||
name: "",
|
name: "",
|
@ -11,17 +11,17 @@ import Mic from '@mui/icons-material/Mic';
|
|||||||
import VideocamOff from '@mui/icons-material/VideocamOff';
|
import VideocamOff from '@mui/icons-material/VideocamOff';
|
||||||
import Videocam from '@mui/icons-material/Videocam';
|
import Videocam from '@mui/icons-material/Videocam';
|
||||||
|
|
||||||
import { GlobalContext } from "./GlobalContext.js";
|
import { GlobalContext, GlobalData } from "./GlobalContext";
|
||||||
const debug = true;
|
const debug = true;
|
||||||
|
|
||||||
/* Proxy object so we can pass in srcObject to <audio> */
|
/* Proxy object so we can pass in srcObject to <audio> */
|
||||||
const Video = ({ srcObject, local, ...props }) => {
|
const Video = ({ srcObject, local, ...props }: any) => {
|
||||||
const refVideo = useRef(null);
|
const refVideo = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refVideo.current) {
|
if (!refVideo.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ref = refVideo.current;
|
const ref: any = refVideo.current;
|
||||||
if (debug) console.log('media-control - video <video> bind');
|
if (debug) console.log('media-control - video <video> bind');
|
||||||
ref.srcObject = srcObject;
|
ref.srcObject = srcObject;
|
||||||
if (local) {
|
if (local) {
|
||||||
@ -37,12 +37,12 @@ const Video = ({ srcObject, local, ...props }) => {
|
|||||||
return <video ref={refVideo} {...props} />;
|
return <video ref={refVideo} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaAgent = ({setPeers}) => {
|
const MediaAgent = ({setPeers}: any) => {
|
||||||
const { name, ws } = useContext(GlobalContext);
|
const { name, ws } = useContext<GlobalData>(GlobalContext);
|
||||||
const [ peers ] = useState({});
|
const [ peers ] = useState<any>({});
|
||||||
const [stream, setStream] = useState(undefined);
|
const [stream, setStream] = useState<any>(undefined);
|
||||||
|
|
||||||
const onTrack = useCallback((event) => {
|
const onTrack = useCallback((event: any) => {
|
||||||
const connection = event.target;
|
const connection = event.target;
|
||||||
console.log("media-agent - ontrack", event);
|
console.log("media-agent - ontrack", event);
|
||||||
for (let peer in peers) {
|
for (let peer in peers) {
|
||||||
@ -58,12 +58,15 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
}, [peers, setPeers]);
|
}, [peers, setPeers]);
|
||||||
const refOnTrack = useRef(onTrack);
|
const refOnTrack = useRef(onTrack);
|
||||||
|
|
||||||
const sendMessage = useCallback((data) => {
|
const sendMessage = useCallback((data: any) => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ws.send(JSON.stringify(data));
|
ws.send(JSON.stringify(data));
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
const onWsMessage = useCallback((event) => {
|
const onWsMessage = useCallback((event: any) => {
|
||||||
const addPeer = (config) => {
|
const addPeer = (config: any) => {
|
||||||
console.log('media-agent - Signaling server said to add peer:', config);
|
console.log('media-agent - Signaling server said to add peer:', config);
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@ -82,12 +85,15 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
}
|
}
|
||||||
/* Even if reviving, allocate a new Object so <MediaControl> will
|
/* Even if reviving, allocate a new Object so <MediaControl> will
|
||||||
* have its peer state change and trigger an update from
|
* have its peer state change and trigger an update from
|
||||||
* <PlayerList> */
|
* <PersonList> */
|
||||||
const peer = {
|
const peer = {
|
||||||
name: peer_id,
|
name: peer_id,
|
||||||
hasAudio: config.hasAudio,
|
hasAudio: config.hasAudio,
|
||||||
hasVideo: config.hasVideo,
|
hasVideo: config.hasVideo,
|
||||||
attributes: {},
|
attributes: {},
|
||||||
|
muted: false,
|
||||||
|
videoOn: false,
|
||||||
|
connection: undefined as any
|
||||||
};
|
};
|
||||||
|
|
||||||
if (peer_id in peers) {
|
if (peer_id in peers) {
|
||||||
@ -103,7 +109,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
console.log(`media-agent - addPeer - remote`, peers);
|
console.log(`media-agent - addPeer - remote`, peers);
|
||||||
setPeers(Object.assign({}, peers));
|
setPeers(Object.assign({}, peers));
|
||||||
|
|
||||||
const connection = new RTCPeerConnection({
|
const connection: any = new RTCPeerConnection({
|
||||||
configuration: {
|
configuration: {
|
||||||
offerToReceiveAudio: true,
|
offerToReceiveAudio: true,
|
||||||
offerToReceiveVideo: true
|
offerToReceiveVideo: true
|
||||||
@ -112,29 +118,21 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
urls: "turns:ketrenos.com:5349",
|
urls: "turns:ketrenos.com:5349",
|
||||||
username: "ketra",
|
username: "ketra",
|
||||||
credential: "ketran"
|
credential: "ketran"
|
||||||
},
|
}]
|
||||||
/*
|
} as any);
|
||||||
{
|
|
||||||
urls: "turn:numb.viagenie.ca",
|
|
||||||
username: "james_viagenie@ketrenos.com",
|
|
||||||
credential: "1!viagenie"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
]
|
|
||||||
});
|
|
||||||
peer.connection = connection;
|
peer.connection = connection;
|
||||||
|
|
||||||
connection.addEventListener('connectionstatechange', (event) => {
|
connection.addEventListener('connectionstatechange', (event: any) => {
|
||||||
console.log(`media-agent - connectionstatechange - `,
|
console.log(`media-agent - connectionstatechange - `,
|
||||||
connection.connectionState, event);
|
connection.connectionState, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.addEventListener('negotiationneeded', (event) => {
|
connection.addEventListener('negotiationneeded', (event: any) => {
|
||||||
console.log(`media-agent - negotiationneeded - `,
|
console.log(`media-agent - negotiationneeded - `,
|
||||||
connection.connectionState, event);
|
connection.connectionState, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.addEventListener('icecandidateerror', (event) => {
|
connection.addEventListener('icecandidateerror', (event: any) => {
|
||||||
if (event.errorCode === 701) {
|
if (event.errorCode === 701) {
|
||||||
if (connection.iceGatheringState === 'gathering') {
|
if (connection.iceGatheringState === 'gathering') {
|
||||||
console.log(`media-agent - Unable to reach host: ${event.url}`);
|
console.log(`media-agent - Unable to reach host: ${event.url}`);
|
||||||
@ -144,7 +142,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onicecandidate = (event) => {
|
connection.onicecandidate = (event: any) => {
|
||||||
if (!event.candidate) {
|
if (!event.candidate) {
|
||||||
console.log(`media-agent - icecanditate - gathering is complete: ${connection.connectionState}`);
|
console.log(`media-agent - icecanditate - gathering is complete: ${connection.connectionState}`);
|
||||||
return;
|
return;
|
||||||
@ -171,7 +169,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
connection.ontrack = e => refOnTrack.current(e);
|
connection.ontrack = (e: any) => refOnTrack.current(e);
|
||||||
|
|
||||||
/* Add our local stream */
|
/* Add our local stream */
|
||||||
connection.addStream(stream.media);
|
connection.addStream(stream.media);
|
||||||
@ -186,7 +184,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
if (debug) console.log(`media-agent - Creating RTC offer to ` +
|
if (debug) console.log(`media-agent - Creating RTC offer to ` +
|
||||||
`${peer_id}`);
|
`${peer_id}`);
|
||||||
return connection.createOffer()
|
return connection.createOffer()
|
||||||
.then((local_description) => {
|
.then((local_description: string) => {
|
||||||
if (debug) console.log(`media-agent - Local offer ` +
|
if (debug) console.log(`media-agent - Local offer ` +
|
||||||
`description is: `, local_description);
|
`description is: `, local_description);
|
||||||
return connection.setLocalDescription(local_description)
|
return connection.setLocalDescription(local_description)
|
||||||
@ -201,17 +199,17 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
if (debug) console.log(`media-agent - Offer ` +
|
if (debug) console.log(`media-agent - Offer ` +
|
||||||
`setLocalDescription succeeded`);
|
`setLocalDescription succeeded`);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error: Error) => {
|
||||||
console.error(`media-agent - Offer setLocalDescription failed!`);
|
console.error(`media-agent - Offer setLocalDescription failed!`);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error: Error) => {
|
||||||
console.log(`media-agente - Error sending offer: `, error);
|
console.log(`media-agente - Error sending offer: `, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionDescription = ({ peer_id, session_description }) => {
|
const sessionDescription = ({ peer_id, session_description }: any) => {
|
||||||
const peer = peers[peer_id];
|
const peer = peers[peer_id];
|
||||||
if (!peer) {
|
if (!peer) {
|
||||||
console.error(`media-agent - sessionDescription - ` +
|
console.error(`media-agent - sessionDescription - ` +
|
||||||
@ -226,7 +224,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
if (session_description.type === "offer") {
|
if (session_description.type === "offer") {
|
||||||
if (debug) console.log(`media-agent - sessionDescription - ` +
|
if (debug) console.log(`media-agent - sessionDescription - ` +
|
||||||
`Creating answer`);
|
`Creating answer`);
|
||||||
connection.createAnswer((local_description) => {
|
connection.createAnswer((local_description: string) => {
|
||||||
if (debug) console.log(`media-agent - sessionDescription - ` +
|
if (debug) console.log(`media-agent - sessionDescription - ` +
|
||||||
`Answer description is: `, local_description);
|
`Answer description is: `, local_description);
|
||||||
connection.setLocalDescription(local_description, () => {
|
connection.setLocalDescription(local_description, () => {
|
||||||
@ -243,17 +241,17 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
console.error(`media-agent - sessionDescription - ` +
|
console.error(`media-agent - sessionDescription - ` +
|
||||||
`Answer setLocalDescription failed!`);
|
`Answer setLocalDescription failed!`);
|
||||||
});
|
});
|
||||||
}, (error) => {
|
}, (error: Error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, (error) => {
|
}, (error: Error) => {
|
||||||
console.log(`media-agent - sessionDescription - ` +
|
console.log(`media-agent - sessionDescription - ` +
|
||||||
`setRemoteDescription error: `, error);
|
`setRemoteDescription error: `, error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const removePeer = ({peer_id}) => {
|
const removePeer = ({peer_id}: any) => {
|
||||||
console.log(`media-agent - removePeer - Signaling server said to ` +
|
console.log(`media-agent - removePeer - Signaling server said to ` +
|
||||||
`remove peer ${peer_id}`);
|
`remove peer ${peer_id}`);
|
||||||
if (peer_id in peers) {
|
if (peer_id in peers) {
|
||||||
@ -270,7 +268,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
setPeers(Object.assign({}, peers));
|
setPeers(Object.assign({}, peers));
|
||||||
};
|
};
|
||||||
|
|
||||||
const iceCandidate = ({ peer_id, candidate }) => {
|
const iceCandidate = ({ peer_id, candidate }: any) => {
|
||||||
/**
|
/**
|
||||||
* The offerer will send a number of ICE Candidate blobs to the
|
* The offerer will send a number of ICE Candidate blobs to the
|
||||||
* answerer so they can begin trying to find the best path to one
|
* answerer so they can begin trying to find the best path to one
|
||||||
@ -287,7 +285,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
if (debug) console.log(`media-agent - iceCandidate - ` +
|
if (debug) console.log(`media-agent - iceCandidate - ` +
|
||||||
`Successfully added Ice Candidate for ${peer_id}`);
|
`Successfully added Ice Candidate for ${peer_id}`);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error: Error) => {
|
||||||
console.error(error, peer, candidate);
|
console.error(error, peer, candidate);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -307,7 +305,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
}, [ peers, setPeers, stream, refOnTrack, sendMessage ]);
|
}, [ peers, setPeers, stream, refOnTrack, sendMessage ]);
|
||||||
const refWsMessage = useRef(onWsMessage);
|
const refWsMessage = useRef(onWsMessage);
|
||||||
|
|
||||||
const onWsClose = (event) => {
|
const onWsClose = (event: any) => {
|
||||||
console.log(`media-agent - ${name} Disconnected from signaling server`);
|
console.log(`media-agent - ${name} Disconnected from signaling server`);
|
||||||
/* Tear down all of our peer connections and remove all the
|
/* Tear down all of our peer connections and remove all the
|
||||||
* media divs when we disconnect */
|
* media divs when we disconnect */
|
||||||
@ -340,9 +338,9 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`media-control - Binding to WebSocket`);
|
console.log(`media-control - Binding to WebSocket`);
|
||||||
const cbMessage = e => refWsMessage.current(e);
|
const cbMessage = (e: any) => refWsMessage.current(e);
|
||||||
ws.addEventListener('message', cbMessage);
|
ws.addEventListener('message', cbMessage);
|
||||||
const cbClose = e => refWsClose.current(e);
|
const cbClose = (e: any) => refWsClose.current(e);
|
||||||
ws.addEventListener('close', cbClose);
|
ws.addEventListener('close', cbClose);
|
||||||
return () => {
|
return () => {
|
||||||
ws.removeEventListener('message', cbMessage);
|
ws.removeEventListener('message', cbMessage);
|
||||||
@ -429,9 +427,13 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
/* See Dummy Tracks for more ideas...
|
/* See Dummy Tracks for more ideas...
|
||||||
* https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
|
* https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore
|
||||||
navigator.getUserMedia = (navigator.getUserMedia ||
|
navigator.getUserMedia = (navigator.getUserMedia ||
|
||||||
|
// @ts-ignore
|
||||||
navigator.webkitGetUserMedia ||
|
navigator.webkitGetUserMedia ||
|
||||||
|
// @ts-ignore
|
||||||
navigator.mozGetUserMedia ||
|
navigator.mozGetUserMedia ||
|
||||||
|
// @ts-ignore
|
||||||
navigator.msGetUserMedia);
|
navigator.msGetUserMedia);
|
||||||
|
|
||||||
return navigator.mediaDevices
|
return navigator.mediaDevices
|
||||||
@ -471,7 +473,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
audio: context.audio
|
audio: context.audio
|
||||||
});
|
});
|
||||||
|
|
||||||
if (context.video) {
|
if (context.video && context.media) {
|
||||||
console.log("media-agent - Access granted to audio/video");
|
console.log("media-agent - Access granted to audio/video");
|
||||||
context.media.getVideoTracks().forEach((track) => {
|
context.media.getVideoTracks().forEach((track) => {
|
||||||
track.applyConstraints({
|
track.applyConstraints({
|
||||||
@ -485,7 +487,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
"max": 240
|
"max": 240
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} as any);
|
||||||
});
|
});
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
@ -494,7 +496,14 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
const canvas = Object.assign(document.createElement("canvas"), {
|
const canvas = Object.assign(document.createElement("canvas"), {
|
||||||
width, height
|
width, height
|
||||||
});
|
});
|
||||||
canvas.getContext('2d').fillRect(0, 0, width, height);
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
const stream = canvas.captureStream();
|
const stream = canvas.captureStream();
|
||||||
return Object.assign(stream.getVideoTracks()[0], {
|
return Object.assign(stream.getVideoTracks()[0], {
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -503,7 +512,7 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
|
|
||||||
const silence = () => {
|
const silence = () => {
|
||||||
const ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
const ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||||
const dst = oscillator.connect(ctx.createMediaStreamDestination());
|
const dst: any = oscillator.connect(ctx.createMediaStreamDestination());
|
||||||
oscillator.start();
|
oscillator.start();
|
||||||
return Object.assign(dst.stream.getAudioTracks()[0], {
|
return Object.assign(dst.stream.getAudioTracks()[0], {
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -517,13 +526,17 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
let canvas = Object.assign(document.createElement("canvas"), {
|
let canvas = Object.assign(document.createElement("canvas"), {
|
||||||
width, height
|
width, height
|
||||||
});
|
});
|
||||||
canvas.getContext('2d').fillRect(0, 0, width, height);
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
let stream = canvas.captureStream();
|
let stream = canvas.captureStream();
|
||||||
return Object.assign(stream.getVideoTracks()[0], {
|
return Object.assign(stream.getVideoTracks()[0], {
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
context.media = new MediaStream([context.media, black()]);
|
context.media = new MediaStream([context.media, black()] as any);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,19 +574,23 @@ const MediaAgent = ({setPeers}) => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaControl = ({isSelf, peer, className}) => {
|
const MediaControl = ({isSelf, peer, className}: any) => {
|
||||||
const [media, setMedia] = useState(undefined);
|
const [media, setMedia] = useState<any>(undefined);
|
||||||
const [muted, setMuted] = useState(undefined);
|
const [muted, setMuted] = useState<boolean>(false);
|
||||||
const [videoOn, setVideoOn] = useState(undefined);
|
const [videoOn, setVideoOn] = useState<boolean>(false);
|
||||||
const [target, setTarget] = useState();
|
const [target, setTarget] = useState<HTMLElement>();
|
||||||
const [frame, setFrame] = useState({
|
const [frame, setFrame] = useState({
|
||||||
translate: [0, 0],
|
translate: [0, 0],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (peer && peer.name) {
|
if (peer && peer.name) {
|
||||||
setTarget(document.querySelector(
|
const el: HTMLElement | null = document.querySelector(
|
||||||
`.MediaControl[data-peer="${peer.name}"]`));
|
`.MediaControl[data-peer="${peer.name}"]`);
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTarget(el);
|
||||||
}
|
}
|
||||||
}, [setTarget, peer]);
|
}, [setTarget, peer]);
|
||||||
|
|
||||||
@ -592,7 +609,7 @@ const MediaControl = ({isSelf, peer, className}) => {
|
|||||||
|
|
||||||
console.log(`media-control - render`);
|
console.log(`media-control - render`);
|
||||||
|
|
||||||
const toggleMute = (event) => {
|
const toggleMute = (event: any) => {
|
||||||
if (debug) console.log(`media-control - toggleMute - ${peer.name}`,
|
if (debug) console.log(`media-control - toggleMute - ${peer.name}`,
|
||||||
!muted);
|
!muted);
|
||||||
peer.muted = !muted;
|
peer.muted = !muted;
|
||||||
@ -600,12 +617,12 @@ const MediaControl = ({isSelf, peer, className}) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleVideo = (event) => {
|
const toggleVideo = (event: any) => {
|
||||||
if (debug) console.log(`media-control - toggleVideo - ${peer.name}`,
|
if (debug) console.log(`media-control - toggleVideo - ${peer.name}`,
|
||||||
!videoOn);
|
!videoOn);
|
||||||
peer.videoOn = !videoOn;
|
peer.videoOn = !videoOn;
|
||||||
if (peer.videoOn) {
|
if (peer.videoOn && media) {
|
||||||
const video = document.querySelector(`video[data-id="${media.name}"`);
|
const video: HTMLVideoElement | null = document.querySelector(`video[data-id="${media.name}"`);
|
||||||
if (video) {
|
if (video) {
|
||||||
video.play();
|
video.play();
|
||||||
}
|
}
|
||||||
@ -620,7 +637,7 @@ const MediaControl = ({isSelf, peer, className}) => {
|
|||||||
}
|
}
|
||||||
if (media.attributes.srcObject) {
|
if (media.attributes.srcObject) {
|
||||||
console.log(`media-control - audio enable - ${peer.name}:${!muted}`);
|
console.log(`media-control - audio enable - ${peer.name}:${!muted}`);
|
||||||
media.attributes.srcObject.getAudioTracks().forEach((track) => {
|
media.attributes.srcObject.getAudioTracks().forEach((track: any) => {
|
||||||
track.enabled = media.hasAudio && !muted;
|
track.enabled = media.hasAudio && !muted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -633,7 +650,7 @@ const MediaControl = ({isSelf, peer, className}) => {
|
|||||||
}
|
}
|
||||||
if (media.attributes.srcObject) {
|
if (media.attributes.srcObject) {
|
||||||
console.log(`media-control - video enable - ${peer.name}:${videoOn}`);
|
console.log(`media-control - video enable - ${peer.name}:${videoOn}`);
|
||||||
media.attributes.srcObject.getVideoTracks().forEach((track) => {
|
media.attributes.srcObject.getVideoTracks().forEach((track: any) => {
|
||||||
track.enabled = media.hasVideo && videoOn;
|
track.enabled = media.hasVideo && videoOn;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -677,24 +694,24 @@ const MediaControl = ({isSelf, peer, className}) => {
|
|||||||
edge={true}
|
edge={true}
|
||||||
zoom={1}
|
zoom={1}
|
||||||
origin={false}
|
origin={false}
|
||||||
onDragStart={e => {
|
onDragStart={(e: any) => {
|
||||||
e.set(frame.translate);
|
e.set(frame.translate);
|
||||||
}}
|
}}
|
||||||
onDrag={e => {
|
onDrag={(e: any) => {
|
||||||
frame.translate = e.beforeTranslate;
|
frame.translate = e.beforeTranslate;
|
||||||
}}
|
}}
|
||||||
onResizeStart={e => {
|
onResizeStart={(e: any) => {
|
||||||
e.setOrigin(["%", "%"]);
|
e.setOrigin(["%", "%"]);
|
||||||
e.dragStart && e.dragStart.set(frame.translate);
|
e.dragStart && e.dragStart.set(frame.translate);
|
||||||
}}
|
}}
|
||||||
onResize={e => {
|
onResize={(e: any) => {
|
||||||
const { translate, rotate, transformOrigin } = frame;
|
const { translate } = frame;
|
||||||
e.target.style.width = `${e.width}px`;
|
e.target.style.width = `${e.width}px`;
|
||||||
e.target.style.height = `${e.height}px`;
|
e.target.style.height = `${e.height}px`;
|
||||||
e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
|
e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
|
||||||
}}
|
}}
|
||||||
onRender={e => {
|
onRender={(e: any) => {
|
||||||
const { translate, rotate, transformOrigin } = frame;
|
const { translate } = frame;
|
||||||
//e.target.style.transformOrigin = transformOrigin;
|
//e.target.style.transformOrigin = transformOrigin;
|
||||||
e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
|
e.target.style.transform = `translate(${translate[0]}px, ${translate[1]}px)`;
|
||||||
}}
|
}}
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
.PlayerColor {
|
.PersonColor {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -13,7 +13,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerColor > div {
|
.PersonColor > div {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
13
client/src/PersonColor.tsx
Normal file
13
client/src/PersonColor.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Avatar from '@mui/material/Avatar'
|
||||||
|
|
||||||
|
import "./PersonColor.css";
|
||||||
|
|
||||||
|
const PersonColor = ({ color }: any) => {
|
||||||
|
return (
|
||||||
|
<Avatar className={['PersonColor', color].join(' ')}/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PersonColor };
|
@ -1,4 +1,4 @@
|
|||||||
.PlayerList {
|
.PersonList {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
@ -7,14 +7,14 @@
|
|||||||
margin: 0.25rem 0.25rem 0.25rem 0;
|
margin: 0.25rem 0.25rem 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Name {
|
.PersonList .Name {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .NoNetwork {
|
.PersonList .NoNetwork {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
@ -25,29 +25,29 @@
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Unselected {
|
.PersonList .Unselected {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Player name in the Unselected list... */
|
/* Person name in the Unselected list... */
|
||||||
.PlayerList .Unselected > div:nth-child(2) > div > div:first-child {
|
.PersonList .Unselected > div:nth-child(2) > div > div:first-child {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Unselected > div:nth-child(2) {
|
.PersonList .Unselected > div:nth-child(2) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Unselected > div:nth-child(2) > div {
|
.PersonList .Unselected > div:nth-child(2) > div {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -59,36 +59,36 @@
|
|||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Unselected .Self {
|
.PersonList .Unselected .Self {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector .PlayerColor {
|
.PersonList .PersonSelector .PersonColor {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector {
|
.PersonList .PersonSelector {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector.MuiList-padding {
|
.PersonList .PersonSelector.MuiList-padding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector .MuiTypography-body1 {
|
.PersonList .PersonSelector .MuiTypography-body1 {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
/* white-space: nowrap;*/
|
/* white-space: nowrap;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector .MuiTypography-body2 {
|
.PersonList .PersonSelector .MuiTypography-body2 {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector .PlayerEntry {
|
.PersonList .PersonSelector .PersonEntry {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -103,7 +103,7 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector .PlayerEntry > div:first-child {
|
.PersonList .PersonSelector .PersonEntry > div:first-child {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -111,27 +111,27 @@
|
|||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerEntry[data-selectable=true]:hover {
|
.PersonList .PersonEntry[data-selectable=true]:hover {
|
||||||
border-color: rgba(0,0,0,0.5);
|
border-color: rgba(0,0,0,0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Players .PlayerToggle {
|
.PersonList .Persons .PersonToggle {
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerName {
|
.PersonList .PersonName {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Players > * {
|
.PersonList .Persons > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Players .nameInput {
|
.PersonList .Persons .nameInput {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,21 @@
|
|||||||
import React, { useState, useEffect, useContext, useRef } from "react";
|
import React, { useState, useEffect, useContext, useRef } from "react";
|
||||||
import { Paper, List } from '@mui/material';
|
import { Paper, List } from '@mui/material';
|
||||||
|
|
||||||
import "./PlayerList.css";
|
import "./PersonList.css";
|
||||||
import { PlayerColor } from './PlayerColor.js';
|
import { PersonColor } from './PersonColor';
|
||||||
import { MediaAgent, MediaControl } from "./MediaControl.js";
|
import { MediaAgent, MediaControl } from "./MediaControl";
|
||||||
|
|
||||||
import { GlobalContext } from "./GlobalContext.js";
|
import { GlobalContext } from "./GlobalContext";
|
||||||
|
|
||||||
const PlayerList = () => {
|
const PersonList = () => {
|
||||||
const { ws, name } = useContext(GlobalContext);
|
const { ws, name } = useContext(GlobalContext);
|
||||||
const [players, setPlayers] = useState({});
|
const [persons, setPersons] = useState<any>({});
|
||||||
const [unselected, setUneslected] = useState([]);
|
const [unselected, setUneslected] = useState([]);
|
||||||
const [state, setState] = useState('lobby');
|
const [state, setState] = useState('lobby');
|
||||||
const [color, setColor] = useState(undefined);
|
const [color, setColor] = useState<string>('');
|
||||||
const [peers, setPeers] = useState({});
|
const [peers, setPeers] = useState<any>({});
|
||||||
|
|
||||||
const onWsMessage = (event) => {
|
const onWsMessage = (event: any) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'game-update':
|
case 'game-update':
|
||||||
@ -25,19 +25,19 @@ const PlayerList = () => {
|
|||||||
setUneslected(data.update.unselected);
|
setUneslected(data.update.unselected);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('players' in data.update) {
|
if ('persons' in data.update) {
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let key in data.update.players) {
|
for (let key in data.update.persons) {
|
||||||
if (data.update.players[key].name === name) {
|
if (data.update.persons[key].name === name) {
|
||||||
found = true;
|
found = true;
|
||||||
setColor(key);
|
setColor(key);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
setColor(undefined);
|
setColor('');
|
||||||
}
|
}
|
||||||
setPlayers(data.update.players);
|
setPersons(data.update.persons);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('state' in data.update && data.update.state !== state) {
|
if ('state' in data.update && data.update.state !== state) {
|
||||||
@ -56,7 +56,7 @@ const PlayerList = () => {
|
|||||||
if (!ws) {
|
if (!ws) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cbMessage = e => refWsMessage.current(e);
|
const cbMessage = (e: any) => refWsMessage.current(e);
|
||||||
ws.addEventListener('message', cbMessage);
|
ws.addEventListener('message', cbMessage);
|
||||||
return () => {
|
return () => {
|
||||||
ws.removeEventListener('message', cbMessage);
|
ws.removeEventListener('message', cbMessage);
|
||||||
@ -69,11 +69,14 @@ const PlayerList = () => {
|
|||||||
}
|
}
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'get',
|
type: 'get',
|
||||||
fields: [ 'state', 'players', 'unselected' ]
|
fields: [ 'state', 'persons', 'unselected' ]
|
||||||
}));
|
}));
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
const toggleSelected = (key) => {
|
const toggleSelected = (key: string) => {
|
||||||
|
if (!ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'set',
|
type: 'set',
|
||||||
field: 'color',
|
field: 'color',
|
||||||
@ -81,16 +84,16 @@ const PlayerList = () => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerElements = [];
|
const playerElements: any[] = [];
|
||||||
|
|
||||||
const inLobby = state === 'lobby';
|
const inLobby = state === 'lobby';
|
||||||
const sortedPlayers = [];
|
const sortedPersons = [];
|
||||||
|
|
||||||
for (let key in players) {
|
for (let key in persons) {
|
||||||
sortedPlayers.push(players[key]);
|
sortedPersons.push(persons[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortPlayers = (A, B) => {
|
const sortPersons = (A: any, B: any) => {
|
||||||
/* active player first */
|
/* active player first */
|
||||||
if (A.name === name) {
|
if (A.name === name) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -99,7 +102,7 @@ const PlayerList = () => {
|
|||||||
return +1;
|
return +1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sort active players first */
|
/* Sort active persons first */
|
||||||
if (A.name && !B.name) {
|
if (A.name && !B.name) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -111,10 +114,10 @@ const PlayerList = () => {
|
|||||||
return A.color.localeCompare(B.color);
|
return A.color.localeCompare(B.color);
|
||||||
};
|
};
|
||||||
|
|
||||||
sortedPlayers.sort(sortPlayers);
|
sortedPersons.sort(sortPersons);
|
||||||
|
|
||||||
/* Array of just names... */
|
/* Array of just names... */
|
||||||
unselected.sort((A, B) => {
|
unselected.sort((A: any, B: any) => {
|
||||||
/* active player first */
|
/* active player first */
|
||||||
if (A === name) {
|
if (A === name) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -126,20 +129,20 @@ const PlayerList = () => {
|
|||||||
return A.localeCompare(B);
|
return A.localeCompare(B);
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoClass = sortedPlayers.length <= 2 ? 'Medium' : 'Small';
|
const videoClass = sortedPersons.length <= 2 ? 'Medium' : 'Small';
|
||||||
|
|
||||||
sortedPlayers.forEach(player => {
|
sortedPersons.forEach(player => {
|
||||||
const name = player.name;
|
const name = player.name;
|
||||||
const selectable = inLobby && (player.status === 'Not active' || color === player.color);
|
const selectable = inLobby && (player.status === 'Not active' || color === player.color);
|
||||||
playerElements.push(
|
playerElements.push(
|
||||||
<div
|
<div
|
||||||
data-selectable={selectable}
|
data-selectable={selectable}
|
||||||
data-selected={player.color === color}
|
data-selected={player.color === color}
|
||||||
className="PlayerEntry"
|
className="PersonEntry"
|
||||||
onClick={() => { inLobby && selectable && toggleSelected(player.color) }}
|
onClick={() => { inLobby && selectable && toggleSelected(player.color) }}
|
||||||
key={`player-${player.color}`}>
|
key={`player-${player.color}`}>
|
||||||
<div>
|
<div>
|
||||||
<PlayerColor color={player.color}/>
|
<PersonColor color={player.color}/>
|
||||||
<div className="Name">{name ? name : 'Available' }</div>
|
<div className="Name">{name ? name : 'Available' }</div>
|
||||||
{ name && !player.live && <div className="NoNetwork"></div> }
|
{ name && !player.live && <div className="NoNetwork"></div> }
|
||||||
</div>
|
</div>
|
||||||
@ -158,9 +161,9 @@ const PlayerList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className={`PlayerList ${videoClass}`}>
|
<Paper className={`PersonList ${videoClass}`}>
|
||||||
<MediaAgent setPeers={setPeers}/>
|
<MediaAgent setPeers={setPeers}/>
|
||||||
<List className="PlayerSelector">
|
<List className="PersonSelector">
|
||||||
{ playerElements }
|
{ playerElements }
|
||||||
</List>
|
</List>
|
||||||
{ unselected && unselected.length !== 0 && <div className="Unselected">
|
{ unselected && unselected.length !== 0 && <div className="Unselected">
|
||||||
@ -173,4 +176,4 @@ const PlayerList = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PlayerList };
|
export { PersonList };
|
@ -1,4 +1,4 @@
|
|||||||
.PlayerName {
|
.PersonName {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -6,12 +6,12 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerName > .nameInput {
|
.PersonName > .nameInput {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerName > Button {
|
.PersonName > Button {
|
||||||
background: lightblue;
|
background: lightblue;
|
||||||
}
|
}
|
@ -1,26 +1,26 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import "./PlayerName.css";
|
import "./PersonName.css";
|
||||||
import { TextField, Button } from '@material-ui/core';
|
import { TextField, Button } from '@mui/material';
|
||||||
|
|
||||||
const PlayerName = ({ name, setName }) => {
|
const PersonName = ({ name, setName }: any) => {
|
||||||
const [edit, setEdit] = useState(name);
|
const [edit, setEdit] = useState(name);
|
||||||
|
|
||||||
const sendName = () => {
|
const sendName = () => {
|
||||||
setName(edit);
|
setName(edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameChange = (event) => {
|
const nameChange = (event: any) => {
|
||||||
setEdit(event.target.value);
|
setEdit(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameKeyPress = (event) => {
|
const nameKeyPress = (event: any) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
setName(edit ? edit : name);
|
setName(edit ? edit : name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="PlayerName">
|
<div className="PersonName">
|
||||||
<TextField className="nameInput"
|
<TextField className="nameInput"
|
||||||
onChange={nameChange}
|
onChange={nameChange}
|
||||||
onKeyPress={nameKeyPress}
|
onKeyPress={nameKeyPress}
|
||||||
@ -33,4 +33,4 @@ const PlayerName = ({ name, setName }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PlayerName };
|
export { PersonName };
|
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
import React from "react";
|
|
||||||
import Avatar from '@mui/material/Avatar'
|
|
||||||
|
|
||||||
import "./PlayerColor.css";
|
|
||||||
|
|
||||||
const PlayerColor = ({ color }) => {
|
|
||||||
return (
|
|
||||||
<Avatar className={['PlayerColor', color].join(' ')}/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { PlayerColor };
|
|
@ -1,204 +0,0 @@
|
|||||||
.PlayersStatus {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: absolute;
|
|
||||||
color: #d0d0d0;
|
|
||||||
pointer-events: none;
|
|
||||||
align-items: flex-end;
|
|
||||||
right: 0;
|
|
||||||
width: 16rem;
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerStatus * {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer {
|
|
||||||
align-items: flex-start;
|
|
||||||
pointer-events: all;
|
|
||||||
right: auto;
|
|
||||||
bottom: 8rem; /* 1rem over top of Resource cards in hand */
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Player:not(:last-child) {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer .Player {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Player .Who {
|
|
||||||
color: white;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer .Who {
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayerStatus.ActivePlayer .Resource {
|
|
||||||
margin: 0.5rem 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .What {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .What > div {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer .What {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .PlayerColor {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Shrunken {
|
|
||||||
position: relative;
|
|
||||||
height: 4.75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Normal {
|
|
||||||
position: relative;
|
|
||||||
height: 7rem;
|
|
||||||
display: flex;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.PlayersStatus .BoardPieces {
|
|
||||||
align-items: flex-end;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer .BoardPieces {
|
|
||||||
align-items: flex-start;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Shrunken .BoardPieces {
|
|
||||||
align-items: flex-end;
|
|
||||||
right: 0;
|
|
||||||
transform-origin: 100% 100%;
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Resource {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
height: 3rem;
|
|
||||||
width: 2.1rem;
|
|
||||||
background-size: contain;
|
|
||||||
pointer-events: none;
|
|
||||||
margin: 0.75rem 0.5rem 0 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
filter: brightness(150%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Has {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Placard {
|
|
||||||
/*
|
|
||||||
width: 9.4em;
|
|
||||||
height: 11.44em;
|
|
||||||
*/
|
|
||||||
width: 3rem;
|
|
||||||
height: 3.64rem;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 108%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
margin: 0 0 0 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus.ActivePlayer .Placard {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Placard > div.Right {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
top: -0.5rem;
|
|
||||||
right: -1.25rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid white;
|
|
||||||
background-color: rgb(36, 148, 46);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1rem;
|
|
||||||
filter: brightness(150%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Points {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Points .Resource {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid #444;
|
|
||||||
background-size: 130%;
|
|
||||||
margin: 0 0 0 -0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Points .Resource:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Stack:not(:first-child) {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Resource > div {
|
|
||||||
position: absolute;
|
|
||||||
top: -0.5rem;
|
|
||||||
right: -0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid white;
|
|
||||||
background-color: rgb(36, 148, 46);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayersStatus .Points b {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
import React, { useContext, useState, useMemo, useRef, useEffect } from "react";
|
|
||||||
import equal from "fast-deep-equal";
|
|
||||||
|
|
||||||
import "./PlayersStatus.css";
|
|
||||||
import { BoardPieces } from './BoardPieces.js';
|
|
||||||
import { Resource } from './Resource.js';
|
|
||||||
import { PlayerColor } from './PlayerColor.js';
|
|
||||||
import { Placard } from './Placard.js';
|
|
||||||
import { GlobalContext } from './GlobalContext.js';
|
|
||||||
|
|
||||||
const Player = ({ player, onClick, reverse, color,
|
|
||||||
largestArmy, isSelf, longestRoad, mostPorts, mostDeveloped }) => {
|
|
||||||
if (!player) {
|
|
||||||
return <>You are an observer.</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const developmentCards = player.unplayed
|
|
||||||
? <Resource label={true} type={'progress-back'}
|
|
||||||
count={player.unplayed} disabled/>
|
|
||||||
: undefined;
|
|
||||||
const resourceCards = player.resources
|
|
||||||
? <Resource label={true} type={'resource-back'}
|
|
||||||
count={player.resources} disabled/>
|
|
||||||
: undefined;
|
|
||||||
const armyCards = player.army
|
|
||||||
? <Resource label={true} type={'army-1'} count={player.army} disabled/>
|
|
||||||
: undefined;
|
|
||||||
let points = <></>;
|
|
||||||
if (player.points && reverse) {
|
|
||||||
points = <><b>{player.points}</b><Resource type={'progress-back'}
|
|
||||||
count={player.points} disabled/></>;
|
|
||||||
} else if (player.points) {
|
|
||||||
points = <><Resource type={'progress-back'} count={player.points}
|
|
||||||
disabled/><b>{player.points}</b></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mostPortsPlacard = mostPorts && mostPorts === color ?
|
|
||||||
<Placard
|
|
||||||
disabled
|
|
||||||
active={false}
|
|
||||||
type='port-of-call'
|
|
||||||
count={player.ports}
|
|
||||||
/> : undefined;
|
|
||||||
|
|
||||||
const mostDevelopedPlacard = mostDeveloped && mostDeveloped === color ?
|
|
||||||
<Placard
|
|
||||||
disabled
|
|
||||||
active={false}
|
|
||||||
type='most-developed'
|
|
||||||
count={player.developmentCards}
|
|
||||||
/> : undefined;
|
|
||||||
|
|
||||||
const longestRoadPlacard = longestRoad && longestRoad === color ?
|
|
||||||
<Placard
|
|
||||||
disabled
|
|
||||||
active={false}
|
|
||||||
type='longest-road'
|
|
||||||
count={player.longestRoad}
|
|
||||||
/> : undefined;
|
|
||||||
|
|
||||||
const largestArmyPlacard = largestArmy && largestArmy === color ?
|
|
||||||
<Placard
|
|
||||||
disabled
|
|
||||||
active={false}
|
|
||||||
type='largest-army'
|
|
||||||
count={player.army}
|
|
||||||
/> : undefined;
|
|
||||||
|
|
||||||
return <div className="Player">
|
|
||||||
<div className="Who">
|
|
||||||
<PlayerColor color={color}/>{player.name}
|
|
||||||
</div>
|
|
||||||
<div className="What">
|
|
||||||
{ isSelf &&
|
|
||||||
<div className="LongestRoad">
|
|
||||||
Longest road: {player.longestRoad ? player.longestRoad : 0}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className="Points">{points}</div>
|
|
||||||
{ (largestArmy || longestRoad || armyCards || resourceCards || developmentCards || mostPorts || mostDeveloped) && <>
|
|
||||||
<div className="Has">
|
|
||||||
{ !reverse && <>
|
|
||||||
{ mostDevelopedPlacard }
|
|
||||||
{ mostPortsPlacard }
|
|
||||||
{ largestArmyPlacard }
|
|
||||||
{ longestRoadPlacard }
|
|
||||||
{ !largestArmyPlacard && armyCards }
|
|
||||||
{ developmentCards }
|
|
||||||
{ resourceCards }
|
|
||||||
</> }
|
|
||||||
{ reverse && <>
|
|
||||||
{ resourceCards }
|
|
||||||
{ developmentCards }
|
|
||||||
{ !largestArmyPlacard && armyCards }
|
|
||||||
{ longestRoadPlacard }
|
|
||||||
{ largestArmyPlacard }
|
|
||||||
{ mostPortsPlacard }
|
|
||||||
{ mostDevelopedPlacard }
|
|
||||||
</> }
|
|
||||||
</div>
|
|
||||||
</> }
|
|
||||||
</div>
|
|
||||||
<div className={`${onClick ? 'Normal' : 'Shrunken'}`}>
|
|
||||||
<BoardPieces onClick={onClick} player={player}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlayersStatus = ({ active }) => {
|
|
||||||
const { ws } = useContext(GlobalContext);
|
|
||||||
const [players, setPlayers] = useState(undefined);
|
|
||||||
const [color, setColor] = useState(undefined);
|
|
||||||
const [largestArmy, setLargestArmy] = useState(undefined);
|
|
||||||
const [longestRoad, setLongestRoad] = useState(undefined);
|
|
||||||
const [mostPorts, setMostPorts] = useState(undefined);
|
|
||||||
const [mostDeveloped, setMostDeveloped] = useState(undefined);
|
|
||||||
const fields = useMemo(() => [
|
|
||||||
'players', 'color', 'longestRoad', 'largestArmy', 'mostPorts', 'mostDeveloped'
|
|
||||||
], []);
|
|
||||||
const onWsMessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
switch (data.type) {
|
|
||||||
case 'game-update':
|
|
||||||
console.log(`players-status - game-update: `, data.update);
|
|
||||||
if ('players' in data.update && !equal(players, data.update.players)) {
|
|
||||||
setPlayers(data.update.players);
|
|
||||||
}
|
|
||||||
if ('color' in data.update && data.update.color !== color) {
|
|
||||||
setColor(data.update.color);
|
|
||||||
}
|
|
||||||
if ('longestRoad' in data.update
|
|
||||||
&& data.update.longestRoad !== longestRoad) {
|
|
||||||
setLongestRoad(data.update.longestRoad);
|
|
||||||
}
|
|
||||||
if ('largestArmy' in data.update
|
|
||||||
&& data.update.largestArmy !== largestArmy) {
|
|
||||||
setLargestArmy(data.update.largestArmy);
|
|
||||||
}
|
|
||||||
if ('mostDeveloped' in data.update
|
|
||||||
&& data.update.mostDeveloped !== mostDeveloped) {
|
|
||||||
setMostDeveloped(data.update.mostDeveloped);
|
|
||||||
}
|
|
||||||
if ('mostPorts' in data.update
|
|
||||||
&& data.update.mostPorts !== mostPorts) {
|
|
||||||
setMostPorts(data.update.mostPorts);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const refWsMessage = useRef(onWsMessage);
|
|
||||||
useEffect(() => { refWsMessage.current = onWsMessage; });
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ws) { return; }
|
|
||||||
const cbMessage = e => refWsMessage.current(e);
|
|
||||||
ws.addEventListener('message', cbMessage);
|
|
||||||
return () => { ws.removeEventListener('message', cbMessage); }
|
|
||||||
}, [ws, refWsMessage]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ws) { return; }
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'get',
|
|
||||||
fields
|
|
||||||
}));
|
|
||||||
}, [ws, fields]);
|
|
||||||
|
|
||||||
if (!players) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildItem = () => {
|
|
||||||
console.log(`player-status - build-item`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let elements;
|
|
||||||
if (active) {
|
|
||||||
elements = <Player
|
|
||||||
player={players[color]}
|
|
||||||
onClick={buildItem}
|
|
||||||
reverse
|
|
||||||
largestArmy={largestArmy}
|
|
||||||
longestRoad={longestRoad}
|
|
||||||
mostPorts={mostPorts}
|
|
||||||
mostDeveloped={mostDeveloped}
|
|
||||||
isSelf={active}
|
|
||||||
key={`PlayerStatus-${color}`}
|
|
||||||
color={color}/>;
|
|
||||||
} else {
|
|
||||||
elements = Object.getOwnPropertyNames(players)
|
|
||||||
.filter(key => color !== key)
|
|
||||||
.map(key => {
|
|
||||||
return <Player
|
|
||||||
player={players[key]}
|
|
||||||
largestArmy={largestArmy}
|
|
||||||
longestRoad={longestRoad}
|
|
||||||
mostPorts={mostPorts}
|
|
||||||
mostDeveloped={mostDeveloped}
|
|
||||||
key={`PlayerStatus-${key}}`}
|
|
||||||
color={key}/>;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`PlayersStatus ${active ? 'ActivePlayer' : ''}`}>
|
|
||||||
{ elements }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PlayersStatus };
|
|
104
client/tsconfig.json
Normal file
104
client/tsconfig.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,16 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
||||||
- ./logs:/home/user/logs #
|
- ./logs:/home/user/logs #
|
||||||
|
- ./db:/home/user/db
|
||||||
# Hot mount client and server for dynamic changes in DEVELOPMENT
|
# Hot mount client and server for dynamic changes in DEVELOPMENT
|
||||||
- ./scripts:/opt/scripts # Hot update via bind
|
- ./scripts:/opt/scripts # Hot update via bind
|
||||||
- ./server/app.js:/home/user/server/app.js
|
|
||||||
- ./server/config:/home/user/server/config
|
|
||||||
- ./server/package.json:/home/user/server/package.json
|
- ./server/package.json:/home/user/server/package.json
|
||||||
- ./server/package-lock.json:/home/user/server/package-lock.json
|
- ./server/package-lock.json:/home/user/server/package-lock.json
|
||||||
|
- ./server/app.js:/home/user/server/app.js
|
||||||
|
- ./server/config:/home/user/server/config
|
||||||
|
- ./server/db:/home/user/server/db
|
||||||
- ./server/routes:/home/user/server/routes
|
- ./server/routes:/home/user/server/routes
|
||||||
|
- ./server/lib:/home/user/server/lib
|
||||||
- ./server/nginx.conf:/etc/nginx/sites-available/default
|
- ./server/nginx.conf:/etc/nginx/sites-available/default
|
||||||
- ./client/package.json:/home/user/client/package.json
|
- ./client/package.json:/home/user/client/package.json
|
||||||
- ./client/package-lock.json:/home/user/client/package-lock.json
|
- ./client/package-lock.json:/home/user/client/package-lock.json
|
||||||
|
@ -32,7 +32,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
||||||
- ./scripts:/opt/scripts # Hot update via bind
|
- ./db:/home/user/db
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:19876:80
|
- 127.0.0.1:19876:80
|
||||||
|
|
||||||
|
@ -5,3 +5,4 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
- /etc/nginx/ssl:/etc/nginx/ssl:ro # Use host web keys
|
||||||
- ./logs:/home/user/logs #
|
- ./logs:/home/user/logs #
|
||||||
|
- ./db:/home/user/db
|
||||||
|
@ -83,7 +83,7 @@ process.on('SIGINT', () => {
|
|||||||
server.close(() => process.exit(1));
|
server.close(() => process.exit(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
require("./db/chat").then(function(db) {
|
require("./db/chats").then(function(db) {
|
||||||
chatDB = db;
|
chatDB = db;
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return require("./db/users").then(function(db) {
|
return require("./db/users").then(function(db) {
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
"storage": "../db/users.db",
|
"storage": "../db/users.db",
|
||||||
"logging" : false,
|
"logging" : false,
|
||||||
"timezone": "+00:00"
|
"timezone": "+00:00"
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"storage": "../db/chats.db",
|
||||||
|
"logging": false,
|
||||||
|
"timezone": "+00:00"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
let basePath = process.env.REACT_APP_basePath;
|
let basePath = process.env.CHAT_BASE;
|
||||||
|
console.log(basePath);
|
||||||
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
|
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
|
||||||
if (basePath == "//") {
|
if (basePath == "//") {
|
||||||
basePath = "/";
|
basePath = "/";
|
||||||
|
@ -4,7 +4,7 @@ const express = require("express"),
|
|||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
url = require("url"),
|
url = require("url"),
|
||||||
config = require("config"),
|
config = require("config"),
|
||||||
basePath = require("../basepath");
|
basePath = require("../lib/basepath");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user