diff --git a/src/actions/index.js b/src/actions/index.js
index 0a1172d..020dcd7 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -197,6 +197,7 @@ export function receiveChatMessage(
text: string,
country: string,
channel: number,
+ isPing: boolean,
): Action {
return {
type: 'RECEIVE_CHAT_MESSAGE',
@@ -204,6 +205,7 @@ export function receiveChatMessage(
text,
country,
channel,
+ isPing,
};
}
diff --git a/src/actions/types.js b/src/actions/types.js
index b70700a..66ebbfc 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -59,6 +59,7 @@ export type Action =
text: string,
country: string,
channel: number,
+ isPing: boolean,
}
| { type: 'RECEIVE_CHAT_HISTORY', data: Array }
| { type: 'SET_CHAT_CHANNEL', channelId: number }
diff --git a/src/client.js b/src/client.js
index e83d9a2..6ddb9b4 100644
--- a/src/client.js
+++ b/src/client.js
@@ -29,6 +29,7 @@ import ProtocolClient from './socket/ProtocolClient';
function init() {
initRenderer(store, false);
+ let nameRegExp = null;
ProtocolClient.on('pixelUpdate', ({
i, j, offset, color,
}) => {
@@ -40,8 +41,12 @@ function init() {
ProtocolClient.on('onlineCounter', ({ online }) => {
store.dispatch(receiveOnline(online));
});
+ ProtocolClient.on('setWsName', (name) => {
+ nameRegExp = new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g');
+ });
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) => {
store.dispatch(receiveChatHistory(data));
diff --git a/src/components/Chat.jsx b/src/components/Chat.jsx
index 3c6d623..bf90942 100644
--- a/src/components/Chat.jsx
+++ b/src/components/Chat.jsx
@@ -9,82 +9,26 @@ import React, {
import useStayScrolled from 'react-stay-scrolled';
import { connect } from 'react-redux';
-import { MAX_CHAT_MESSAGES } from '../core/constants';
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 { colorFromText, splitChatMessage } from '../core/utils';
+import splitChatMessage from '../core/chatMessageFilter';
-function ChatMessage({ name, msgArray, country }) {
- 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';
- }
-
- return (
-
- {
- (!isInfo)
- && (
-
-
{
- e.target.onerror = null;
- e.target.src = './cf/xx.gif';
- }}
- />
-
-
- {name}
-
- :
-
- )
- }
- {
- msgArray.map((msgPart) => {
- const [type, txt] = msgPart;
- if (type === 't') {
- return ({txt});
- } if (type === 'c') {
- return ({txt});
- } if (type === 'p') {
- return ({txt});
- } if (type === 'm') {
- return (
- {txt}
- );
- }
- return null;
- })
- }
-
- );
-}
-
-const Chat = ({ chatMessages, chatChannel, ownName }) => {
+const Chat = ({
+ chatMessages,
+ chatChannel,
+ ownName,
+ open,
+ setChannel,
+}) => {
const listRef = useRef();
+ const inputRef = useRef();
+ const [inputMessage, setInputMessage] = useState('');
const [selection, setSelection] = useState(null);
const [nameRegExp, setNameRegExp] = useState(null);
const { stayScrolled } = useStayScrolled(listRef, {
@@ -95,7 +39,7 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
useLayoutEffect(() => {
stayScrolled();
- }, [channelMessages.slice(-1)]);
+ }, [channelMessages]);
useEffect(() => {
if (channelMessages.length === MAX_CHAT_MESSAGES) {
@@ -110,6 +54,25 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
setNameRegExp(regExp);
}, [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 (
{
name={message[0]}
msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]}
+ insertText={(txt) => padToInputMessage(txt)}
/>
))
}
-
+ {(ownName) ? (
+
+
+
+ ) : (
+
+ You must be logged in to chat
+
+ )}
);
};
@@ -140,4 +146,15 @@ function mapStateToProps(state: State) {
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);
diff --git a/src/components/ChatInput.jsx b/src/components/ChatInput.jsx
deleted file mode 100644
index 97558dd..0000000
--- a/src/components/ChatInput.jsx
+++ /dev/null
@@ -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 (
-
-
-
- );
- }
- return (
-
- You must be logged in to chat
-
- );
- }
-}
-
-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);
diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx
new file mode 100644
index 0000000..6eab3e5
--- /dev/null
+++ b/src/components/ChatMessage.jsx
@@ -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 (
+
+ {
+ (!isInfo)
+ && (
+
+
{
+ e.target.onerror = null;
+ e.target.src = './cf/xx.gif';
+ }}
+ />
+
+ {
+ insertText(`@${name} `);
+ }}
+ >
+ {name}
+
+ :
+
+ )
+ }
+ {
+ msgArray.map((msgPart) => {
+ const [type, txt] = msgPart;
+ if (type === 't') {
+ return ({txt});
+ } if (type === 'c') {
+ return ({txt});
+ } 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 (
+ {txt}
+ );
+ } if (type === 'm') {
+ return (
+ {txt}
+ );
+ }
+ return null;
+ })
+ }
+
+ );
+}
+
+export default ChatMessage;
diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js
index f72aa68..6fa594f 100644
--- a/src/core/ChatProvider.js
+++ b/src/core/ChatProvider.js
@@ -13,7 +13,7 @@ import {
} from './config';
-class ChatProvider {
+export class ChatProvider {
/*
* TODO:
* history really be saved in redis
@@ -187,13 +187,13 @@ class ChatProvider {
webSockets.broadcastChatMessage(name, message, country, channelId, sendapi);
}
- /*
- * 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) {
+ static automute(name, channelId = 0) {
ChatProvider.mute(name, channelId, 60);
+ webSockets.broadcastChatMessage(
+ 'info',
+ `${name} has been muted for spam for 60min`,
+ channelId,
+ );
}
static async checkIfMuted(user) {
@@ -202,7 +202,8 @@ class ChatProvider {
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);
if (!id) {
return `Couldn't find user ${name}`;
@@ -230,7 +231,8 @@ class ChatProvider {
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);
if (!id) {
return `Couldn't find user ${name}`;
diff --git a/src/core/chatMessageFilter.js b/src/core/chatMessageFilter.js
new file mode 100644
index 0000000..4275bc2
--- /dev/null
+++ b/src/core/chatMessageFilter.js
@@ -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;
diff --git a/src/core/utils.js b/src/core/utils.js
index ff1edf1..52c960c 100644
--- a/src/core/utils.js
+++ b/src/core/utils.js
@@ -226,56 +226,3 @@ export function colorFromText(str: string) {
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;
-}
diff --git a/src/socket/ProtocolClient.js b/src/socket/ProtocolClient.js
index 7bf5047..7e22ddb 100644
--- a/src/socket/ProtocolClient.js
+++ b/src/socket/ProtocolClient.js
@@ -173,6 +173,7 @@ class ProtocolClient extends EventEmitter {
} else {
// string = name
this.name = data;
+ this.emit('setWsName', data);
}
}
diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js
index 2313848..d0a0302 100644
--- a/src/socket/SocketServer.js
+++ b/src/socket/SocketServer.js
@@ -18,7 +18,7 @@ import CoolDownPacket from './packets/CoolDownPacket';
import ChangedMe from './packets/ChangedMe';
import OnlineCounter from './packets/OnlineCounter';
-import chatProvider from '../core/ChatProvider';
+import chatProvider, { ChatProvider } from '../core/ChatProvider';
import authenticateClient from './verifyClient';
import WebSocketEvents from './WebSocketEvents';
import webSockets from './websockets';
@@ -261,19 +261,21 @@ class SocketServer extends WebSocketEvents {
message,
channelId,
);
- if (errorMsg) {
- ws.send(JSON.stringify(['info', errorMsg, 'il', channelId]));
- }
- if (ws.last_message && ws.last_message === message) {
- ws.message_repeat += 1;
- if (ws.message_repeat >= 3) {
- logger.info(`User ${ws.name} got automuted`);
- chatProvider.automute(ws.name, channelId);
+ if (!errorMsg) {
+ // automute on repeated message spam
+ if (ws.last_message && ws.last_message === message) {
+ ws.message_repeat += 1;
+ if (ws.message_repeat >= 4) {
+ logger.info(`User ${ws.name} got automuted`);
+ ChatProvider.automute(ws.name, channelId);
+ ws.message_repeat = 0;
+ }
+ } else {
ws.message_repeat = 0;
+ ws.last_message = message;
}
} else {
- ws.message_repeat = 0;
- ws.last_message = message;
+ ws.send(JSON.stringify(['info', errorMsg, 'il', channelId]));
}
logger.info(
`Received chat message ${message} from ${ws.name} / ${ws.user.ip}`,
diff --git a/src/store/audio.js b/src/store/audio.js
index 5610b14..b87f0af 100644
--- a/src/store/audio.js
+++ b/src/store/audio.js
@@ -174,8 +174,9 @@ export default (store) => (next) => (action) => {
case 'RECEIVE_CHAT_MESSAGE': {
if (!chatNotify) break;
+ const { isPing } = action;
const { chatChannel } = state.gui;
- if (action.channel !== chatChannel) {
+ if (!isPing && action.channel !== chatChannel) {
break;
}
@@ -184,8 +185,9 @@ export default (store) => (next) => (action) => {
oscillatorNode.type = 'sine';
oscillatorNode.frequency.setValueAtTime(310, context.currentTime);
+ const freq = (isPing) ? 540 : 355;
oscillatorNode.frequency.exponentialRampToValueAtTime(
- 355,
+ freq,
context.currentTime + 0.025,
);
diff --git a/src/store/notifications.js b/src/store/notifications.js
index d06d474..4a91856 100644
--- a/src/store/notifications.js
+++ b/src/store/notifications.js
@@ -7,40 +7,65 @@
export default (store) => (next) => (action) => {
try {
- switch (action.type) {
- case 'PLACE_PIXEL': {
- if (window.Notification
- && Notification.permission !== 'granted'
- && Notification.permission !== 'denied'
- ) {
- Notification.requestPermission();
- }
- break;
- }
-
- 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()) {
+ if (!document.hasFocus()) {
+ switch (action.type) {
+ case 'RECEIVE_ME':
+ case 'PLACE_PIXEL': {
+ if (window.Notification
+ && Notification.permission !== 'granted'
+ && Notification.permission !== 'denied'
+ ) {
+ Notification.requestPermission();
+ }
break;
}
- if (window.Notification && Notification.permission === 'granted') {
- // eslint-disable-next-line no-unused-vars
- 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;
- }
+ case 'COOLDOWN_END': {
+ const state = store.getState();
- default:
- // nothing
+ // do not notify if last cooldown end was <15s ago
+ 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) {
// eslint-disable-next-line no-console
diff --git a/src/styles/default.css b/src/styles/default.css
index 3ccdbc1..7befbcd 100644
--- a/src/styles/default.css
+++ b/src/styles/default.css
@@ -389,7 +389,6 @@ tr:nth-child(even) {
.chatname {
color: #4B0000;
font-size: 13px;
- user-select: all;
}
.chatmsg {
color: #030303;