import _ from 'underscore';
import sanitizeHtml from 'sanitize-html';
import he from 'he';
// TODO: Remove in v2.0
let warned = false;
export default class Chat {
constructor(darkwire, socket) {
this.usernamesInMemory = [];
this.FADE_TIME = 150; // ms
this.TYPING_TIMER_LENGTH = 400; // ms
this.typing = false;
this.lastTypingTime = null;
this.darkwire = darkwire;
this.socket = socket;
this.messages = $('.messages'); // Messages area
this.inputMessage = $('.inputMessage'); // Input message input box
this.chatPage = $('.chat.page');
this.bindEvents();
}
// Log a message
log(message, options) {
let html = options && options.html === true || false;
let classNames = options && options.classNames ? options.classNames : '';
let $el;
let matchedUsernames = this.checkIfUsername(message.split(' '));
if (matchedUsernames.length > 0) {
for (let i = 0; i < matchedUsernames.length; i++) {
let usernameContainer = $('')
.text(matchedUsernames[i])
.css('color', this.getUsernameColor(matchedUsernames[i]));
// Match only the username
let matchedUsernameOnly = new RegExp('(' + matchedUsernames[i] + ')(?![^<]*>|[^<>]*<\/)', 'gm');
message = message.replace(matchedUsernameOnly, usernameContainer.prop('outerHTML'));
}
}
if (options && options.error) {
$el = $('
').addClass(`log ${classNames}`).html('ERROR: ' + message);
} else if (options && options.warning) {
$el = $('').addClass(`log ${classNames}`).html('WARNING: ' + message);
} else if (options && options.notice) {
$el = $('').addClass(`log ${classNames}`).html('NOTICE: ' + message);
} else if (options && options.info) {
$el = $('').addClass(`log ${classNames}`).html(message);
} else {
$el = $('').addClass(`log ${classNames}`).html(message);
}
this.addMessageElement($el, options);
}
checkIfUsername(words) {
let matchedUsernames = [];
this.darkwire.users.forEach((user) => {
let usernameMatch = new RegExp('^' + user.username + '$');
for (let i = 0; i < words.length; i++) {
let exactMatch = words[i].match(usernameMatch) || false;
let usernameInMemory = this.usernamesInMemory.indexOf(words[i]) > -1;
if (exactMatch && exactMatch.length > -1 || usernameInMemory) {
if (!usernameInMemory) {
this.usernamesInMemory.push(words[i]);
}
matchedUsernames.push(words[i]);
}
}
});
return matchedUsernames;
}
// Gets the color of a username through our hash function
getUsernameColor(username) {
const COLORS = [
'#e21400', '#ffe400', '#ff8f00',
'#58dc00', '#dd9cff', '#4ae8c4',
'#3b88eb', '#f47777', '#EEACB7',
'#D3FF3E', '#99CC33', '#999933',
'#996633', '#B8D5B8', '#7FFF38',
'#FADBBC', '#FAE2B7', '#EBE8AF',
];
// Compute hash code
let hash = 7;
for (let i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
let index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
bindEvents() {
var _this = this;
// Select message input when clicking message body, unless selecting text
this.messages.on('click', () => {
if (!this.getSelectedText()) {
this.inputMessage.focus();
}
});
this.inputMessage.on('input propertychange paste change', function() {
_this.updateTyping();
let message = $(this).val().trim();
if (message.length) {
$('#send-message-btn').addClass('active');
} else {
$('#send-message-btn').removeClass('active');
}
});
}
// Updates the typing event
updateTyping() {
if (this.darkwire.connected) {
if (!this.typing) {
this.typing = true;
this.socket.emit('typing');
}
this.lastTypingTime = (new Date()).getTime();
setTimeout(() => {
let typingTimer = (new Date()).getTime();
let timeDiff = typingTimer - this.lastTypingTime;
if (timeDiff >= this.TYPING_TIMER_LENGTH && this.typing) {
this.socket.emit('stop typing');
this.typing = false;
}
}, this.TYPING_TIMER_LENGTH);
}
}
addChatTyping(data) {
data.typing = true;
data.message = 'is typing';
this.addChatMessage(data);
}
getSelectedText() {
let text = '';
if (typeof window.getSelection != 'undefined') {
text = window.getSelection().toString();
} else if (typeof document.selection != 'undefined' && document.selection.type == 'Text') {
text = document.selection.createRange().text;
}
return text;
}
getTypingMessages(data) {
return $('.typing.message').filter(function(i) {
return $(this).data('username') === data.username;
});
}
removeChatTyping(data) {
this.getTypingMessages(data).fadeOut(function() {
$(this).remove();
});
}
slashCommands(trigger) {
let validCommands = [];
let expectedParams = 0;
const triggerCommands = [{
command: 'nick',
description: 'Changes nickname.',
paramaters: ['{username}'],
multiple: false,
usage: '/nick {username}',
action: () => {
let newUsername = trigger.params[0] || false;
if (newUsername.toString().length > 16) {
return this.log('Username cannot be greater than 16 characters.', {error: true});
}
// Remove things that arent digits or chars
newUsername = newUsername.replace(/[^A-Za-z0-9]/g, '-');
if (!newUsername.match(/^[A-Z]/i)) {
return this.log('Username must start with a letter.', {error: true});
}
if (!warned) {
warned = true;
return this.log('Changing your username is currently in beta and your new username will be sent over the wire in plain text, unecrypted. This will be fixed in v2.0. If you really want to do this, type the command again.',
{
warning: true,
classNames: 'change-username-warning'
});
}
this.darkwire.updateUsername(newUsername).then((socketData) => {
let modifiedSocketData = {
username: window.username,
newUsername: socketData.username
};
this.socket.emit('update user', modifiedSocketData);
window.username = username = socketData.username;
}).catch((err) => {
return this.log(err, {error: true});
});
}
}, {
command: 'help',
description: 'Shows a list of commands.',
paramaters: [],
multiple: false,
usage: '/help',
action: () => {
validCommands = validCommands.map((command) => {
return '/' + command;
});
this.log('Valid commands: ' + validCommands.sort().join(', '), {notice: true});
}
}, {
command: 'me',
description: 'Invoke virtual action',
paramaters: ['{action}'],
multiple: true,
usage: '/me {action}',
action: () => {
expectedParams = 100;
let actionMessage = trigger.params.join(' ');
this.darkwire.encodeMessage(actionMessage, 'action').then((socketData) => {
this.addChatMessage({
username: username,
message: actionMessage,
messageType: 'action'
});
this.socket.emit('new message', socketData);
}).catch((err) => {
console.log(err);
});
}
}];
const color = () => {
const hexTex = new RegExp(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i);
};
triggerCommands.forEach((command) => {
validCommands.push(command.command);
});
let commandToTrigger = _.findWhere(triggerCommands, {command: trigger.command});
if (commandToTrigger) {
expectedParams = commandToTrigger.paramaters.length;
if (expectedParams && trigger.params.length > expectedParams || expectedParams && trigger.params.length < expectedParams) {
if ((!commandToTrigger.multple && trigger.params.length < 1) || (trigger.params.length >= 1 && trigger.params[0] === '')) {
return this.log('Missing or too many paramater. Usage: ' + commandToTrigger.usage, {error: true});
}
}
return commandToTrigger.action.call();
}
this.log(trigger.command + ' is not a valid command. Type /help for a list of valid commands.', {error: true});
return false;
}
executeCommand(trigger) {
trigger = trigger || false;
if (trigger) {
let command = trigger.command;
this.slashCommands(trigger);
}
}
parseCommand(cleanedMessage) {
let trigger = {
command: null,
params: []
};
if (cleanedMessage.indexOf('/') === 0) {
this.inputMessage.val('');
let parsedCommand = cleanedMessage.replace('/', '').split(' ');
trigger.command = sanitizeHtml(parsedCommand[0]) || null;
// Get params
if (parsedCommand.length >= 2) {
for (let i = 1; i < parsedCommand.length; i++) {
trigger.params.push(parsedCommand[i]);
}
}
return trigger;
}
return false;
}
addChatMessage(data, options) {
if (!data.message.trim().length) {
return;
}
let messageType = data.messageType || 'text';
// Don't fade the message in if there is an 'X was typing'
let $typingMessages = this.getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
let $usernameDiv = $('')
.text(data.username)
.css('color', this.getUsernameColor(data.username));
let $messageBodyDiv = $('');
if (messageType === 'text' || messageType === 'action') {
if (messageType === 'action') {
$usernameDiv.css('color','').prepend('*');
}
let unescapedMessage = unescape(data.message);
let lineBreaks = /<br \/>/g;
unescapedMessage = unescapedMessage.replace(lineBreaks, '
');
$messageBodyDiv.html(unescapedMessage);
} else {
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
}
let typingClass = data.typing ? 'typing' : '';
let actionClass = data.messageType === 'action' ? 'action' : '';
let $messageDiv = $('')
.data('username', data.username)
.addClass(typingClass)
.addClass(actionClass)
.append($usernameDiv, $messageBodyDiv);
this.addMessageElement($messageDiv, options);
}
addMessageElement(el, options) {
let $el = $(el);
if (!options) {
options = {};
}
$el.hide().fadeIn(this.FADE_TIME);
this.messages.append($el);
this.messages[0].scrollTop = this.messages[0].scrollHeight; // minus 60 for key
}
replaceMessage(id, message) {
let container = $(id);
if (container) {
container.html(message);
}
}
}