'use strict'; /** * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const padutils = require('./pad_utils').padutils; const padcookie = require('./pad_cookie').padcookie; const Tinycon = require('tinycon/tinycon'); const hooks = require('./pluginfw/hooks'); const padeditor = require('./pad_editor').padeditor; exports.chat = (() => { let isStuck = false; let userAndChat = false; let chatMentions = 0; return { show() { $('#chaticon').removeClass('visible'); $('#chatbox').addClass('visible'); this.scrollDown(true); chatMentions = 0; Tinycon.setBubble(0); $('.chat-gritter-msg').each(function () { $.gritter.remove(this.id); }); }, focus: () => { setTimeout(() => { $('#chatinput').focus(); }, 100); }, // Make chat stick to right hand side of screen stickToScreen(fromInitialCall) { if (pad.settings.hideChat) { return; } this.show(); isStuck = (!isStuck || fromInitialCall); $('#chatbox').hide(); // Add timeout to disable the chatbox animations setTimeout(() => { $('#chatbox, .sticky-container').toggleClass('stickyChat', isStuck); $('#chatbox').css('display', 'flex'); }, 0); padcookie.setPref('chatAlwaysVisible', isStuck); $('#options-stickychat').prop('checked', isStuck); }, chatAndUsers(fromInitialCall) { const toEnable = $('#options-chatandusers').is(':checked'); if (toEnable || !userAndChat || fromInitialCall) { this.stickToScreen(true); $('#options-stickychat').prop('checked', true); $('#options-chatandusers').prop('checked', true); $('#options-stickychat').prop('disabled', 'disabled'); userAndChat = true; } else { $('#options-stickychat').prop('disabled', false); userAndChat = false; } padcookie.setPref('chatAndUsers', userAndChat); $('#users, .sticky-container') .toggleClass('chatAndUsers popup-show stickyUsers', userAndChat); $('#chatbox').toggleClass('chatAndUsersChat', userAndChat); }, hide() { // decide on hide logic based on chat window being maximized or not if ($('#options-stickychat').prop('checked')) { this.stickToScreen(); $('#options-stickychat').prop('checked', false); } else { $('#chatcounter').text('0'); $('#chaticon').addClass('visible'); $('#chatbox').removeClass('visible'); } }, scrollDown(force) { if ($('#chatbox').hasClass('visible')) { if (force || !this.lastMessage || !this.lastMessage.position() || this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) { // if we use a slow animate here we can have a race condition // when a users focus can not be moved away from the last message recieved. $('#chattext').animate( {scrollTop: $('#chattext')[0].scrollHeight}, {duration: 400, queue: false}); this.lastMessage = $('#chattext > p').eq(-1); } } }, send() { const text = $('#chatinput').val(); if (text.replace(/\s+/, '').length === 0) return; this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', text}); $('#chatinput').val(''); }, addMessage(msg, increment, isHistoryAdd) { // correct the time msg.time += this._pad.clientTimeOffset; if (!msg.userId) { /* * If, for a bug or a database corruption, the message coming from the * server does not contain the userId field (see for example #3731), * let's be defensive and replace it with "unknown". */ msg.userId = 'unknown'; console.warn( 'The "userId" field of a chat message coming from the server was not present. ' + 'Replacing with "unknown". This may be a bug or a database corruption.'); } const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => { if (c === '.') return '-'; return `z${c.charCodeAt(0)}z`; })}`; // the hook args const ctx = { authorName: msg.userName != null ? msg.userName : html10n.get('pad.userlist.unnamed'), author: msg.userId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), sticky: false, timestamp: msg.time, timeStr: (() => { let minutes = `${new Date(msg.time).getMinutes()}`; let hours = `${new Date(msg.time).getHours()}`; if (minutes.length === 1) minutes = `0${minutes}`; if (hours.length === 1) hours = `0${hours}`; return `${hours}:${minutes}`; })(), duration: 4000, }; // is the users focus already in the chatbox? const alreadyFocused = $('#chatinput').is(':focus'); // does the user already have the chatbox open? const chatOpen = $('#chatbox').hasClass('visible'); // does this message contain this user's name? (is the curretn user mentioned?) const myName = $('#myusernameedit').val(); const wasMentioned = ctx.text.toLowerCase().indexOf(myName.toLowerCase()) !== -1 && myName !== 'undefined'; // If the user was mentioned, make the message sticky if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { chatMentions++; Tinycon.setBubble(chatMentions); ctx.sticky = true; } // Call chat message hook hooks.aCallAll('chatNewMessage', ctx, () => { const cls = authorClass(ctx.author); const chatMsg = $('
')
.attr('data-authorId', ctx.author)
.addClass(cls)
.append($('').text(`${ctx.authorName}:`))
.append($('')
.addClass('time')
.addClass(cls)
// Hook functions are trusted to not introduce an XSS vulnerability by adding
// unescaped user input to ctx.timeStr.
.html(ctx.timeStr))
.append(' ')
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not
// introduce an XSS vulnerability by adding unescaped user input.
.append($(' ')
.append($('').addClass('author-name').text(ctx.authorName))
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted
// to not introduce an XSS vulnerability by adding unescaped user input.
.append(ctx.text),
sticky: ctx.sticky,
time: 5000,
position: 'bottom',
class_name: 'chat-gritter-msg',
});
}
}
});
// Clear the chat mentions when the user clicks on the chat input box
$('#chatinput').click(() => {
chatMentions = 0;
Tinycon.setBubble(0);
});
if (!isHistoryAdd) this.scrollDown();
},
init(pad) {
this._pad = pad;
$('#chatinput').on('keydown', (evt) => {
// If the event is Alt C or Escape & we're already in the chat menu
// Send the users focus back to the pad
if ((evt.altKey === true && evt.which === 67) || evt.which === 27) {
// If we're in chat already..
$(':focus').blur(); // required to do not try to remove!
padeditor.ace.focus(); // Sends focus back to pad
evt.preventDefault();
return false;
}
});
const self = this;
$('body:not(#chatinput)').on('keypress', function (evt) {
if (evt.altKey && evt.which === 67) {
// Alt c focuses on the Chat window
$(this).blur();
self.show();
$('#chatinput').focus();
evt.preventDefault();
}
});
$('#chatinput').keypress((evt) => {
// if the user typed enter, fire the send
if (evt.which === 13 || evt.which === 10) {
evt.preventDefault();
this.send();
}
});
// initial messages are loaded in pad.js' _afterHandshake
$('#chatcounter').text(0);
$('#chatloadmessagesbutton').click(() => {
const start = Math.max(this.historyPointer - 20, 0);
const end = this.historyPointer;
if (start === end) return; // nothing to load
$('#chatloadmessagesbutton').css('display', 'none');
$('#chatloadmessagesball').css('display', 'block');
pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end});
this.historyPointer = start;
});
},
};
})();