1
0

Builds with TypeScript enabled

Signed-off-by: James Ketrenos <james@ketrenos.com>
This commit is contained in:
James Ketrenos 2023-03-30 16:14:27 -07:00
parent 57eef0e9cf
commit 29de35b17f
28 changed files with 965 additions and 590 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
logs/* logs/*
db/*

View File

@ -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": {

View File

@ -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
View 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
View 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
View 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 };

View File

@ -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: "",

View File

@ -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)`;
}} }}

View File

@ -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;

View 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 };

View File

@ -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;
} }

View File

@ -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 };

View File

@ -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;
} }

View File

@ -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 };

View File

@ -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 };

View File

@ -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;
}

View File

@ -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
View 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. */
}
}

0
db/.keep Normal file
View File

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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": {

View File

@ -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 = "/";

View File

@ -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();