mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
346 lines
9.8 KiB
JavaScript
346 lines
9.8 KiB
JavaScript
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 = $('<span/>')
|
|
.text(matchedUsernames[i])
|
|
.css('color', this.getUsernameColor(matchedUsernames[i]));
|
|
message = message.replace(matchedUsernames[i], usernameContainer.prop('outerHTML'));
|
|
}
|
|
}
|
|
|
|
if (options && options.error) {
|
|
$el = $('<li class="log-error">').addClass('log').html(message);
|
|
} else if (options && options.info) {
|
|
$el = $('<li class="log-info">').addClass('log').html(message);
|
|
} else {
|
|
$el = $('<li>').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 = $('<span class="username"/>')
|
|
.text(data.username)
|
|
.css('color', this.getUsernameColor(data.username));
|
|
|
|
let $messageBodyDiv = $('<span class="messageBody">');
|
|
|
|
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, '<br />');
|
|
$messageBodyDiv.html(unescapedMessage);
|
|
} else {
|
|
$messageBodyDiv.html(this.darkwire.addFileToQueue(data));
|
|
}
|
|
|
|
let typingClass = data.typing ? 'typing' : '';
|
|
let actionClass = data.messageType === 'action' ? 'action' : '';
|
|
|
|
let $messageDiv = $('<li class="message"/>')
|
|
.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);
|
|
}
|
|
}
|
|
|
|
}
|