import _ from 'underscore';
import sanitizeHtml from 'sanitize-html';
import he from 'he';
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 $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]));
message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
}
}
if (options && options.error) {
$el = $('
').addClass('log').html(message);
} else if (options && options.info) {
$el = $('').addClass('log').html(message);
} else {
$el = $('').addClass('log').html(message);
}
this.addMessageElement($el, options);
}
checkIfUsername(words) {
let matchedUsernames = [];
this.darkwire.users.forEach((user) => {
let usernameMatch = new RegExp('^' + user.username + '$', 'g');
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', '#d300e7',
'#99FF33', '#99CC33', '#999933',
'#996633', '#993333', '#990033',
];
// 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 > 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-Z0-9]/i)) {
return this.log('Username must start with a letter or number.', {error: true});
}
this.darkwire.updateUsername(window.username, newUsername).then((socketData) => {
let modifiedSocketData = {
username: window.username,
newUsername: socketData.username
};
this.socket.emit('update user', modifiedSocketData);
window.username = username = socketData.username;
});
}
}, {
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(', '), {info: 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) {
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);
}
}
}