201 lines
6.3 KiB
JavaScript
201 lines
6.3 KiB
JavaScript
import React, { useState, useEffect, useContext, useRef, useCallback, useMemo } from "react";
|
|
import Paper from '@material-ui/core/Paper';
|
|
import List from '@material-ui/core/List';
|
|
import ListItem from '@material-ui/core/ListItem';
|
|
import ListItemText from '@material-ui/core/ListItemText';
|
|
import Moment from 'react-moment';
|
|
import TextField from '@material-ui/core/TextField';
|
|
import 'moment-timezone';
|
|
|
|
import "./Chat.css";
|
|
import { PlayerColor } from './PlayerColor.js';
|
|
import { Resource } from './Resource.js';
|
|
import { Dice } from './Dice.js';
|
|
import { GlobalContext } from "./GlobalContext.js";
|
|
|
|
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([]);
|
|
const [startTime, setStartTime] = useState(0);
|
|
|
|
const { ws, name } = useContext(GlobalContext);
|
|
const fields = useMemo(() => [
|
|
'chat', 'startTime'
|
|
], []);
|
|
const onWsMessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
switch (data.type) {
|
|
case 'game-update':
|
|
console.log(`chat - game update`);
|
|
if (data.update.chat && data.update.chat.length !== chat.length) {
|
|
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 => 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) => {
|
|
if (event.key === "Enter") {
|
|
if (!autoScroll) {
|
|
setAutoScroll(true);
|
|
}
|
|
|
|
ws.send(JSON.stringify({ type: 'chat', message: event.target.value }));
|
|
event.target.value = "";
|
|
}
|
|
}, [ws, setAutoScroll, autoScroll]);
|
|
|
|
const chatScroll = (event) => {
|
|
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"),
|
|
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, 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;
|
|
if (punctuation) {
|
|
period = punctuation[1];
|
|
} else {
|
|
period = '';
|
|
}
|
|
let lines = item.message.split('.');
|
|
message = lines
|
|
.filter(line => line.trim() !== '')
|
|
.map((line, index) => {
|
|
/* If the date is in the future, set it to now */
|
|
const dice = line.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/);
|
|
if (dice) {
|
|
if (dice[4]) {
|
|
return <div key={`line-${index}`}>{dice[1]}
|
|
<Dice pips={dice[2]}/>,
|
|
<Dice pips={dice[4]}/>{dice[5]}{ period }</div>;
|
|
} else {
|
|
return <div key={`line-${index}`}>{dice[1]}
|
|
<Dice pips={dice[2]}/>{dice[5]}{ period }</div>;
|
|
}
|
|
}
|
|
|
|
let start = line, message;
|
|
while (start) {
|
|
const resource = start.match(/^(.*)(([0-9]+) (wood|sheep|wheat|stone|brick),?)(.*)$/);
|
|
if (resource) {
|
|
const count = resource[3] ? parseInt(resource[3]) : 1;
|
|
message = <><Resource label={true} count={count}
|
|
type={resource[4]} disabled/>{resource[5]}{message}</>;
|
|
start = resource[1];
|
|
} else {
|
|
message = <>{start}{message}</>;
|
|
start = '';
|
|
}
|
|
}
|
|
return <div key={`line-${index}`}>{ message }{ period }</div>;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<ListItem key={`msg-${item.date}`}
|
|
className={item.color ? '' : 'System'}>
|
|
{ item.color &&
|
|
<PlayerColor 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 }; |