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); } } }