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,
|
||||
country: string,
|
||||
channel: number,
|
||||
isPing: boolean,
|
||||
): Action {
|
||||
return {
|
||||
type: 'RECEIVE_CHAT_MESSAGE',
|
||||
|
@ -204,6 +205,7 @@ export function receiveChatMessage(
|
|||
text,
|
||||
country,
|
||||
channel,
|
||||
isPing,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 (
|
||||
<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 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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<ul
|
||||
|
@ -125,11 +88,54 @@ const Chat = ({ chatMessages, chatChannel, ownName }) => {
|
|||
name={message[0]}
|
||||
msgArray={splitChatMessage(message[1], nameRegExp)}
|
||||
country={message[2]}
|
||||
insertText={(txt) => padToInputMessage(txt)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
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}`;
|
||||
|
|
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}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
// string = name
|
||||
this.name = data;
|
||||
this.emit('setWsName', data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -389,7 +389,6 @@ tr:nth-child(even) {
|
|||
.chatname {
|
||||
color: #4B0000;
|
||||
font-size: 13px;
|
||||
user-select: all;
|
||||
}
|
||||
.chatmsg {
|
||||
color: #030303;
|
||||
|
|
Loading…
Reference in New Issue
Block a user