add on click mentions
add ping notifications
This commit is contained in:
parent
d278edb9b1
commit
7614d5366b
|
@ -197,6 +197,7 @@ export function receiveChatMessage(
|
||||||
text: string,
|
text: string,
|
||||||
country: string,
|
country: string,
|
||||||
channel: number,
|
channel: number,
|
||||||
|
isPing: boolean,
|
||||||
): Action {
|
): Action {
|
||||||
return {
|
return {
|
||||||
type: 'RECEIVE_CHAT_MESSAGE',
|
type: 'RECEIVE_CHAT_MESSAGE',
|
||||||
|
@ -204,6 +205,7 @@ export function receiveChatMessage(
|
||||||
text,
|
text,
|
||||||
country,
|
country,
|
||||||
channel,
|
channel,
|
||||||
|
isPing,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@ export type Action =
|
||||||
text: string,
|
text: string,
|
||||||
country: string,
|
country: string,
|
||||||
channel: number,
|
channel: number,
|
||||||
|
isPing: boolean,
|
||||||
}
|
}
|
||||||
| { type: 'RECEIVE_CHAT_HISTORY', data: Array }
|
| { type: 'RECEIVE_CHAT_HISTORY', data: Array }
|
||||||
| { type: 'SET_CHAT_CHANNEL', channelId: number }
|
| { type: 'SET_CHAT_CHANNEL', channelId: number }
|
||||||
|
|
|
@ -29,6 +29,7 @@ import ProtocolClient from './socket/ProtocolClient';
|
||||||
function init() {
|
function init() {
|
||||||
initRenderer(store, false);
|
initRenderer(store, false);
|
||||||
|
|
||||||
|
let nameRegExp = null;
|
||||||
ProtocolClient.on('pixelUpdate', ({
|
ProtocolClient.on('pixelUpdate', ({
|
||||||
i, j, offset, color,
|
i, j, offset, color,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -40,8 +41,12 @@ function init() {
|
||||||
ProtocolClient.on('onlineCounter', ({ online }) => {
|
ProtocolClient.on('onlineCounter', ({ online }) => {
|
||||||
store.dispatch(receiveOnline(online));
|
store.dispatch(receiveOnline(online));
|
||||||
});
|
});
|
||||||
|
ProtocolClient.on('setWsName', (name) => {
|
||||||
|
nameRegExp = new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g');
|
||||||
|
});
|
||||||
ProtocolClient.on('chatMessage', (name, text, country, channelId) => {
|
ProtocolClient.on('chatMessage', (name, text, country, channelId) => {
|
||||||
store.dispatch(receiveChatMessage(name, text, country, channelId));
|
const isPing = (nameRegExp && text.match(nameRegExp));
|
||||||
|
store.dispatch(receiveChatMessage(name, text, country, channelId, isPing));
|
||||||
});
|
});
|
||||||
ProtocolClient.on('chatHistory', (data) => {
|
ProtocolClient.on('chatHistory', (data) => {
|
||||||
store.dispatch(receiveChatHistory(data));
|
store.dispatch(receiveChatHistory(data));
|
||||||
|
|
|
@ -9,82 +9,26 @@ import React, {
|
||||||
import useStayScrolled from 'react-stay-scrolled';
|
import useStayScrolled from 'react-stay-scrolled';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { MAX_CHAT_MESSAGES } from '../core/constants';
|
|
||||||
import type { State } from '../reducers';
|
import type { State } from '../reducers';
|
||||||
import ChatInput from './ChatInput';
|
import ChatMessage from './ChatMessage';
|
||||||
|
|
||||||
|
import { showUserAreaModal, setChatChannel } from '../actions';
|
||||||
|
import { MAX_CHAT_MESSAGES, CHAT_CHANNELS } from '../core/constants';
|
||||||
|
import ProtocolClient from '../socket/ProtocolClient';
|
||||||
import { saveSelection, restoreSelection } from '../utils/storeSelection';
|
import { saveSelection, restoreSelection } from '../utils/storeSelection';
|
||||||
import { colorFromText, splitChatMessage } from '../core/utils';
|
import splitChatMessage from '../core/chatMessageFilter';
|
||||||
|
|
||||||
|
|
||||||
function ChatMessage({ name, msgArray, country }) {
|
const Chat = ({
|
||||||
if (!name || !msgArray) {
|
chatMessages,
|
||||||
return null;
|
chatChannel,
|
||||||
}
|
ownName,
|
||||||
|
open,
|
||||||
const isInfo = (name === 'info');
|
setChannel,
|
||||||
let className = 'msg';
|
}) => {
|
||||||
if (isInfo) {
|
|
||||||
className += ' info';
|
|
||||||
} else if (msgArray[0][1].charAt(0) === '>') {
|
|
||||||
className += ' greentext';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className="chatmsg">
|
|
||||||
{
|
|
||||||
(!isInfo)
|
|
||||||
&& (
|
|
||||||
<span>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
title={country}
|
|
||||||
src={`${window.assetserver}/cf/${country}.gif`}
|
|
||||||
onError={(e) => {
|
|
||||||
e.target.onerror = null;
|
|
||||||
e.target.src = './cf/xx.gif';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="chatname"
|
|
||||||
style={{
|
|
||||||
color: colorFromText(name),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
msgArray.map((msgPart) => {
|
|
||||||
const [type, txt] = msgPart;
|
|
||||||
if (type === 't') {
|
|
||||||
return (<span className={className}>{txt}</span>);
|
|
||||||
} if (type === 'c') {
|
|
||||||
return (<a href={`./${txt}`}>{txt}</a>);
|
|
||||||
} if (type === 'p') {
|
|
||||||
return (<span className="ping">{txt}</span>);
|
|
||||||
} if (type === 'm') {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className="mention"
|
|
||||||
style={{
|
|
||||||
color: colorFromText(txt),
|
|
||||||
}}
|
|
||||||
>{txt}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chat = ({ chatMessages, chatChannel, ownName }) => {
|
|
||||||
const listRef = useRef();
|
const listRef = useRef();
|
||||||
|
const inputRef = useRef();
|
||||||
|
const [inputMessage, setInputMessage] = useState('');
|
||||||
const [selection, setSelection] = useState(null);
|
const [selection, setSelection] = useState(null);
|
||||||
const [nameRegExp, setNameRegExp] = useState(null);
|
const [nameRegExp, setNameRegExp] = useState(null);
|
||||||
const { stayScrolled } = useStayScrolled(listRef, {
|
const { stayScrolled } = useStayScrolled(listRef, {
|
||||||
|
@ -95,7 +39,7 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
stayScrolled();
|
stayScrolled();
|
||||||
}, [channelMessages.slice(-1)]);
|
}, [channelMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (channelMessages.length === MAX_CHAT_MESSAGES) {
|
if (channelMessages.length === MAX_CHAT_MESSAGES) {
|
||||||
|
@ -110,6 +54,25 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
|
||||||
setNameRegExp(regExp);
|
setNameRegExp(regExp);
|
||||||
}, [ownName]);
|
}, [ownName]);
|
||||||
|
|
||||||
|
function padToInputMessage(txt) {
|
||||||
|
const lastChar = inputMessage.substr(-1);
|
||||||
|
const pad = (lastChar && lastChar !== ' ');
|
||||||
|
let newMsg = inputMessage;
|
||||||
|
if (pad) newMsg += ' ';
|
||||||
|
newMsg += txt;
|
||||||
|
setInputMessage(newMsg);
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inputMessage) return;
|
||||||
|
// send message via websocket
|
||||||
|
ProtocolClient.sendChatMessage(inputMessage, chatChannel);
|
||||||
|
setInputMessage('');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
<ul
|
<ul
|
||||||
|
@ -125,11 +88,54 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
|
||||||
name={message[0]}
|
name={message[0]}
|
||||||
msgArray={splitChatMessage(message[1], nameRegExp)}
|
msgArray={splitChatMessage(message[1], nameRegExp)}
|
||||||
country={message[2]}
|
country={message[2]}
|
||||||
|
insertText={(txt) => padToInputMessage(txt)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<ChatInput />
|
{(ownName) ? (
|
||||||
|
<div classNam="chatinput">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => handleSubmit(e)}
|
||||||
|
style={{ display: 'flex', flexDirection: 'row' }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
style={{ flexGrow: 1, minWidth: 40 }}
|
||||||
|
value={inputMessage}
|
||||||
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder="Chat here"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
style={{ flexGrow: 0 }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
‣
|
||||||
|
</button>
|
||||||
|
<select
|
||||||
|
style={{ flexGrow: 0 }}
|
||||||
|
onChange={(evt) => setChannel(evt.target.selectedIndex)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
CHAT_CHANNELS.map((ch) => (
|
||||||
|
<option selected={ch === chatChannel}>{ch}</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="modallink"
|
||||||
|
onClick={open}
|
||||||
|
style={{ textAlign: 'center', fontSize: 13 }}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
You must be logged in to chat
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -140,4 +146,15 @@ function mapStateToProps(state: State) {
|
||||||
return { chatMessages, chatChannel, ownName: name };
|
return { chatMessages, chatChannel, ownName: name };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Chat);
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
open() {
|
||||||
|
dispatch(showUserAreaModal());
|
||||||
|
},
|
||||||
|
setChannel(channelId) {
|
||||||
|
dispatch(setChatChannel(channelId));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Chat);
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* Chat input field
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import type { State } from '../reducers';
|
|
||||||
import ProtocolClient from '../socket/ProtocolClient';
|
|
||||||
|
|
||||||
import { showUserAreaModal, setChatChannel } from '../actions';
|
|
||||||
import { CHAT_CHANNELS } from '../core/constants';
|
|
||||||
|
|
||||||
class ChatInput extends React.Component {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.state = {
|
|
||||||
message: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(e, channelId) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { message } = this.state;
|
|
||||||
if (!message) return;
|
|
||||||
// send message via websocket
|
|
||||||
ProtocolClient.sendChatMessage(message, channelId);
|
|
||||||
this.setState({
|
|
||||||
message: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name, chatChannel, open, setChannel,
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
message,
|
|
||||||
} = this.state;
|
|
||||||
const selectedChannel = CHAT_CHANNELS[chatChannel];
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
return (
|
|
||||||
<div className="chatinput">
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => { this.handleSubmit(e, chatChannel); }}
|
|
||||||
style={{ display: 'flex', flexDirection: 'row' }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
style={{ flexGrow: 1, minWidth: 40 }}
|
|
||||||
value={message}
|
|
||||||
onChange={(evt) => this.setState({ message: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder="Chat here"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
style={{ flexGrow: 0 }}
|
|
||||||
id="chatmsginput"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
‣
|
|
||||||
</button>
|
|
||||||
<select
|
|
||||||
style={{ flexGrow: 0 }}
|
|
||||||
onChange={(evt) => setChannel(evt.target.selectedIndex)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
CHAT_CHANNELS.map((ch) => (
|
|
||||||
<option selected={ch === selectedChannel}>{ch}</option>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="modallink"
|
|
||||||
onClick={open}
|
|
||||||
style={{ textAlign: 'center', fontSize: 13 }}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
You must be logged in to chat
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
|
||||||
const { name } = state.user;
|
|
||||||
const { chatChannel } = state.gui;
|
|
||||||
return { name, chatChannel };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
open() {
|
|
||||||
dispatch(showUserAreaModal());
|
|
||||||
},
|
|
||||||
setChannel(channelId) {
|
|
||||||
dispatch(setChatChannel(channelId));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatInput);
|
|
102
src/components/ChatMessage.jsx
Normal file
102
src/components/ChatMessage.jsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { colorFromText } from '../core/utils';
|
||||||
|
|
||||||
|
|
||||||
|
function ChatMessage({
|
||||||
|
name,
|
||||||
|
msgArray,
|
||||||
|
country,
|
||||||
|
insertText,
|
||||||
|
}) {
|
||||||
|
if (!name || !msgArray) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInfo = (name === 'info');
|
||||||
|
let className = 'msg';
|
||||||
|
if (isInfo) {
|
||||||
|
className += ' info';
|
||||||
|
} else if (msgArray[0][1].charAt(0) === '>') {
|
||||||
|
className += ' greentext';
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinged = false;
|
||||||
|
return (
|
||||||
|
<p className="chatmsg">
|
||||||
|
{
|
||||||
|
(!isInfo)
|
||||||
|
&& (
|
||||||
|
<span>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
title={country}
|
||||||
|
src={`${window.assetserver}/cf/${country}.gif`}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.onerror = null;
|
||||||
|
e.target.src = './cf/xx.gif';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="chatname"
|
||||||
|
style={{
|
||||||
|
color: colorFromText(name),
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => {
|
||||||
|
insertText(`@${name} `);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
msgArray.map((msgPart) => {
|
||||||
|
const [type, txt] = msgPart;
|
||||||
|
if (type === 't') {
|
||||||
|
return (<span className={className}>{txt}</span>);
|
||||||
|
} if (type === 'c') {
|
||||||
|
return (<a href={`./${txt}`}>{txt}</a>);
|
||||||
|
} if (type === 'p') {
|
||||||
|
if (!pinged) {
|
||||||
|
pinged = true;
|
||||||
|
// TODO notify of ping
|
||||||
|
// ahmm. does that do this on every rerender? :peepowerid:
|
||||||
|
// better put nameRegexp in the store or something
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="ping"
|
||||||
|
style={{
|
||||||
|
color: colorFromText(txt.substr(1)),
|
||||||
|
}}
|
||||||
|
>{txt}</span>
|
||||||
|
);
|
||||||
|
} if (type === 'm') {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="mention"
|
||||||
|
style={{
|
||||||
|
color: colorFromText(txt.substr(1)),
|
||||||
|
}}
|
||||||
|
>{txt}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatMessage;
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from './config';
|
} from './config';
|
||||||
|
|
||||||
|
|
||||||
class ChatProvider {
|
export class ChatProvider {
|
||||||
/*
|
/*
|
||||||
* TODO:
|
* TODO:
|
||||||
* history really be saved in redis
|
* history really be saved in redis
|
||||||
|
@ -187,13 +187,13 @@ class ChatProvider {
|
||||||
webSockets.broadcastChatMessage(name, message, country, channelId, sendapi);
|
webSockets.broadcastChatMessage(name, message, country, channelId, sendapi);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
static automute(name, channelId = 0) {
|
||||||
* that is really just because i do not like to import the class AND the
|
|
||||||
* singleton
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
automute(name, channelId = 0) {
|
|
||||||
ChatProvider.mute(name, channelId, 60);
|
ChatProvider.mute(name, channelId, 60);
|
||||||
|
webSockets.broadcastChatMessage(
|
||||||
|
'info',
|
||||||
|
`${name} has been muted for spam for 60min`,
|
||||||
|
channelId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async checkIfMuted(user) {
|
static async checkIfMuted(user) {
|
||||||
|
@ -202,7 +202,8 @@ class ChatProvider {
|
||||||
return ttl;
|
return ttl;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async mute(name, channelId = 0, timeMin = null) {
|
static async mute(plainName, channelId = 0, timeMin = null) {
|
||||||
|
const name = (plainName.startsWith('@')) ? plainName.substr(1) : plainName;
|
||||||
const id = await User.name2Id(name);
|
const id = await User.name2Id(name);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return `Couldn't find user ${name}`;
|
return `Couldn't find user ${name}`;
|
||||||
|
@ -230,7 +231,8 @@ class ChatProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async unmute(name, channelId = 0) {
|
static async unmute(plainName, channelId = 0) {
|
||||||
|
const name = (plainName.startsWith('@')) ? plainName.substr(1) : plainName;
|
||||||
const id = await User.name2Id(name);
|
const id = await User.name2Id(name);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return `Couldn't find user ${name}`;
|
return `Couldn't find user ${name}`;
|
||||||
|
|
60
src/core/chatMessageFilter.js
Normal file
60
src/core/chatMessageFilter.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* splits chat message into array of what it represents
|
||||||
|
* [[type, text],[type, text], ...]
|
||||||
|
* type:
|
||||||
|
* 't': text
|
||||||
|
* 'p': ping
|
||||||
|
* 'c': coordinates
|
||||||
|
* 'm': mention of somebody else
|
||||||
|
* nameRegExp has to be in the form of:
|
||||||
|
new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g');
|
||||||
|
*/
|
||||||
|
const linkRegExp = /(#[a-z]*,-?[0-9]*,-?[0-9]*(,-?[0-9]+)?)/gi;
|
||||||
|
const linkRegExpFilter = (val, ind) => ((ind % 3) !== 2);
|
||||||
|
const mentionRegExp = /(^|\s+)(@\S+)/g;
|
||||||
|
const spaceFilter = (val, ind) => (val !== ' ' && (ind !== 0 | val !== ''));
|
||||||
|
|
||||||
|
function splitChatMessageRegexp(
|
||||||
|
msgArray,
|
||||||
|
regExp,
|
||||||
|
ident,
|
||||||
|
filter = () => true,
|
||||||
|
) {
|
||||||
|
return msgArray.map((msgPart) => {
|
||||||
|
const [type, part] = msgPart;
|
||||||
|
if (type !== 't') {
|
||||||
|
return [msgPart];
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
.split(regExp)
|
||||||
|
.filter(filter)
|
||||||
|
.map((stri, i) => {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
return ['t', stri];
|
||||||
|
}
|
||||||
|
return [ident, stri];
|
||||||
|
})
|
||||||
|
.filter((el) => !!el[1]);
|
||||||
|
}).flat(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitChatMessage(message, nameRegExp = null) {
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let arr = [['t', message.trim()]];
|
||||||
|
arr = splitChatMessageRegexp(arr, linkRegExp, 'c', linkRegExpFilter);
|
||||||
|
if (nameRegExp) {
|
||||||
|
arr = splitChatMessageRegexp(arr, nameRegExp, 'p', spaceFilter);
|
||||||
|
}
|
||||||
|
arr = splitChatMessageRegexp(arr, mentionRegExp, 'm', spaceFilter);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default splitChatMessage;
|
|
@ -226,56 +226,3 @@ export function colorFromText(str: string) {
|
||||||
|
|
||||||
return `#${'00000'.substring(0, 6 - c.length)}${c}`;
|
return `#${'00000'.substring(0, 6 - c.length)}${c}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* splits chat message into array of what it represents
|
|
||||||
* [[type, text],[type, text], ...]
|
|
||||||
* type:
|
|
||||||
* 't': text
|
|
||||||
* 'p': ping
|
|
||||||
* 'c': coordinates
|
|
||||||
* 'm': mention of somebody else
|
|
||||||
* nameRegExp has to be in the form of:
|
|
||||||
new RegExp(`(^|\\s+)(@${ownName})(\\s+|$)`, 'g');
|
|
||||||
*/
|
|
||||||
const linkRegExp = /(#[a-z]*,-?[0-9]*,-?[0-9]*(,-?[0-9]+)?)/gi;
|
|
||||||
const linkRegExpFilter = (val, ind) => ((ind % 3) !== 2);
|
|
||||||
const mentionRegExp = /(^|\s+)(@\S+)(\s+|$)/g;
|
|
||||||
const spaceFilter = (val) => (val !== ' ');
|
|
||||||
|
|
||||||
function splitChatMessageRegexp(
|
|
||||||
msgArray,
|
|
||||||
regExp,
|
|
||||||
ident,
|
|
||||||
filter = () => true,
|
|
||||||
) {
|
|
||||||
return msgArray.map((msgPart) => {
|
|
||||||
const [type, part] = msgPart;
|
|
||||||
if (type !== 't') {
|
|
||||||
return [msgPart];
|
|
||||||
}
|
|
||||||
return part
|
|
||||||
.split(regExp)
|
|
||||||
.filter(filter)
|
|
||||||
.map((stri, i) => {
|
|
||||||
if (i % 2 === 0) {
|
|
||||||
return ['t', stri];
|
|
||||||
}
|
|
||||||
return [ident, stri];
|
|
||||||
})
|
|
||||||
.filter((el) => !!el)
|
|
||||||
}).flat(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function splitChatMessage(message, nameRegExp = null) {
|
|
||||||
if (!message) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let arr = [['t', message.trim()]];
|
|
||||||
arr = splitChatMessageRegexp(arr, linkRegExp, 'c', linkRegExpFilter);
|
|
||||||
if (nameRegExp) {
|
|
||||||
arr = splitChatMessageRegexp(arr, nameRegExp, 'p', spaceFilter);
|
|
||||||
}
|
|
||||||
arr = splitChatMessageRegexp(arr, mentionRegExp, 'm', spaceFilter);
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
|
@ -173,6 +173,7 @@ class ProtocolClient extends EventEmitter {
|
||||||
} else {
|
} else {
|
||||||
// string = name
|
// string = name
|
||||||
this.name = data;
|
this.name = data;
|
||||||
|
this.emit('setWsName', data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import CoolDownPacket from './packets/CoolDownPacket';
|
||||||
import ChangedMe from './packets/ChangedMe';
|
import ChangedMe from './packets/ChangedMe';
|
||||||
import OnlineCounter from './packets/OnlineCounter';
|
import OnlineCounter from './packets/OnlineCounter';
|
||||||
|
|
||||||
import chatProvider from '../core/ChatProvider';
|
import chatProvider, { ChatProvider } from '../core/ChatProvider';
|
||||||
import authenticateClient from './verifyClient';
|
import authenticateClient from './verifyClient';
|
||||||
import WebSocketEvents from './WebSocketEvents';
|
import WebSocketEvents from './WebSocketEvents';
|
||||||
import webSockets from './websockets';
|
import webSockets from './websockets';
|
||||||
|
@ -261,19 +261,21 @@ class SocketServer extends WebSocketEvents {
|
||||||
message,
|
message,
|
||||||
channelId,
|
channelId,
|
||||||
);
|
);
|
||||||
if (errorMsg) {
|
if (!errorMsg) {
|
||||||
ws.send(JSON.stringify(['info', errorMsg, 'il', channelId]));
|
// automute on repeated message spam
|
||||||
}
|
if (ws.last_message && ws.last_message === message) {
|
||||||
if (ws.last_message && ws.last_message === message) {
|
ws.message_repeat += 1;
|
||||||
ws.message_repeat += 1;
|
if (ws.message_repeat >= 4) {
|
||||||
if (ws.message_repeat >= 3) {
|
logger.info(`User ${ws.name} got automuted`);
|
||||||
logger.info(`User ${ws.name} got automuted`);
|
ChatProvider.automute(ws.name, channelId);
|
||||||
chatProvider.automute(ws.name, channelId);
|
ws.message_repeat = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ws.message_repeat = 0;
|
ws.message_repeat = 0;
|
||||||
|
ws.last_message = message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ws.message_repeat = 0;
|
ws.send(JSON.stringify(['info', errorMsg, 'il', channelId]));
|
||||||
ws.last_message = message;
|
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
`Received chat message ${message} from ${ws.name} / ${ws.user.ip}`,
|
`Received chat message ${message} from ${ws.name} / ${ws.user.ip}`,
|
||||||
|
|
|
@ -174,8 +174,9 @@ export default (store) => (next) => (action) => {
|
||||||
case 'RECEIVE_CHAT_MESSAGE': {
|
case 'RECEIVE_CHAT_MESSAGE': {
|
||||||
if (!chatNotify) break;
|
if (!chatNotify) break;
|
||||||
|
|
||||||
|
const { isPing } = action;
|
||||||
const { chatChannel } = state.gui;
|
const { chatChannel } = state.gui;
|
||||||
if (action.channel !== chatChannel) {
|
if (!isPing && action.channel !== chatChannel) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,8 +185,9 @@ export default (store) => (next) => (action) => {
|
||||||
|
|
||||||
oscillatorNode.type = 'sine';
|
oscillatorNode.type = 'sine';
|
||||||
oscillatorNode.frequency.setValueAtTime(310, context.currentTime);
|
oscillatorNode.frequency.setValueAtTime(310, context.currentTime);
|
||||||
|
const freq = (isPing) ? 540 : 355;
|
||||||
oscillatorNode.frequency.exponentialRampToValueAtTime(
|
oscillatorNode.frequency.exponentialRampToValueAtTime(
|
||||||
355,
|
freq,
|
||||||
context.currentTime + 0.025,
|
context.currentTime + 0.025,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -7,40 +7,65 @@
|
||||||
|
|
||||||
export default (store) => (next) => (action) => {
|
export default (store) => (next) => (action) => {
|
||||||
try {
|
try {
|
||||||
switch (action.type) {
|
if (!document.hasFocus()) {
|
||||||
case 'PLACE_PIXEL': {
|
switch (action.type) {
|
||||||
if (window.Notification
|
case 'RECEIVE_ME':
|
||||||
&& Notification.permission !== 'granted'
|
case 'PLACE_PIXEL': {
|
||||||
&& Notification.permission !== 'denied'
|
if (window.Notification
|
||||||
) {
|
&& Notification.permission !== 'granted'
|
||||||
Notification.requestPermission();
|
&& Notification.permission !== 'denied'
|
||||||
}
|
) {
|
||||||
break;
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'COOLDOWN_END': {
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
// do not notify if last cooldown end was <15s ago
|
|
||||||
const { lastCoolDownEnd } = state.user;
|
|
||||||
if (lastCoolDownEnd && lastCoolDownEnd.getTime() + 15000 > Date.now()) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.Notification && Notification.permission === 'granted') {
|
case 'COOLDOWN_END': {
|
||||||
// eslint-disable-next-line no-unused-vars
|
const state = store.getState();
|
||||||
const notification = new Notification('Your next pixels are ready', {
|
|
||||||
icon: '/tile.png',
|
|
||||||
silent: true,
|
|
||||||
vibrate: [200, 100],
|
|
||||||
body: 'You can now place more on pixelplanet.fun :)',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
// do not notify if last cooldown end was <15s ago
|
||||||
// nothing
|
const { lastCoolDownEnd } = state.user;
|
||||||
|
if (lastCoolDownEnd
|
||||||
|
&& lastCoolDownEnd.getTime() + 15000 > Date.now()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.Notification && Notification.permission === 'granted') {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Notification('Your next pixels are ready', {
|
||||||
|
icon: '/tile.png',
|
||||||
|
silent: true,
|
||||||
|
vibrate: [200, 100],
|
||||||
|
body: 'You can now place more on pixelplanet.fun :)',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RECEIVE_CHAT_MESSAGE': {
|
||||||
|
const state = store.getState();
|
||||||
|
const { chatNotify } = state.audio;
|
||||||
|
if (!chatNotify) break;
|
||||||
|
|
||||||
|
const { isPing } = action;
|
||||||
|
if (!isPing) break;
|
||||||
|
const { name } = action;
|
||||||
|
|
||||||
|
if (window.Notification && Notification.permission === 'granted') {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Notification(`${name} mentioned you`, {
|
||||||
|
icon: '/tile.png',
|
||||||
|
silent: true,
|
||||||
|
vibrate: [200, 100],
|
||||||
|
body: 'You have new messages in chat',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -389,7 +389,6 @@ tr:nth-child(even) {
|
||||||
.chatname {
|
.chatname {
|
||||||
color: #4B0000;
|
color: #4B0000;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
user-select: all;
|
|
||||||
}
|
}
|
||||||
.chatmsg {
|
.chatmsg {
|
||||||
color: #030303;
|
color: #030303;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user