/* global APP, $ */ import { processReplacements } from './Replacement'; import VideoLayout from '../../videolayout/VideoLayout'; import UIUtil from '../../util/UIUtil'; import UIEvents from '../../../../service/UI/UIEvents'; import { smileys } from './smileys'; import { addMessage, markAllRead } from '../../../../react/features/chat'; import { dockToolbox, getToolboxHeight } from '../../../../react/features/toolbox'; let unreadMessages = 0; const sidePanelsContainerId = 'sideToolbarContainer'; const htmlStr = `
`; /** * */ function initHTML() { $(`#${sidePanelsContainerId}`) .append(htmlStr); // make sure we translate the panel, as adding it can be after i18n // library had initialized and translated already present html APP.translation.translateElement($(`#${sidePanelsContainerId}`)); } /** * The container id, which is and the element id. */ const CHAT_CONTAINER_ID = 'chat_container'; /** * Updates visual notification, indicating that a message has arrived. */ function updateVisualNotification() { // XXX The rewrite of the toolbar in React delayed the availability of the // element unreadMessages. In order to work around the delay, I introduced // and utilized unreadMsgSelector in addition to unreadMsgElement. const unreadMsgSelector = $('#unreadMessages'); const unreadMsgElement = unreadMsgSelector.length > 0 ? unreadMsgSelector[0] : undefined; if (unreadMessages && unreadMsgElement) { unreadMsgElement.innerHTML = unreadMessages.toString(); APP.store.dispatch(dockToolbox(true)); const chatButtonElement = document.getElementById('toolbar_button_chat'); const leftIndent = (UIUtil.getTextWidth(chatButtonElement) - UIUtil.getTextWidth(unreadMsgElement)) / 2; const topIndent = ((UIUtil.getTextHeight(chatButtonElement) - UIUtil.getTextHeight(unreadMsgElement)) / 2) - 5; unreadMsgElement.setAttribute( 'style', `top:${topIndent}; left:${leftIndent};`); } else { unreadMsgSelector.html(''); } if (unreadMsgElement) { unreadMsgSelector.parent()[unreadMessages > 0 ? 'show' : 'hide'](); } } /** * Returns the current time in the format it is shown to the user * @returns {string} */ function getCurrentTime(stamp) { const now = stamp ? new Date(stamp) : new Date(); let hour = now.getHours(); let minute = now.getMinutes(); let second = now.getSeconds(); if (hour.toString().length === 1) { hour = `0${hour}`; } if (minute.toString().length === 1) { minute = `0${minute}`; } if (second.toString().length === 1) { second = `0${second}`; } return `${hour}:${minute}:${second}`; } /** * */ function toggleSmileys() { const smileys = $('#smileysContainer'); // eslint-disable-line no-shadow smileys.slideToggle(); $('#usermsg').focus(); } /** * */ function addClickFunction(smiley, number) { smiley.onclick = function addSmileyToMessage() { const usermsg = $('#usermsg'); let message = usermsg.val(); message += smileys[`smiley${number}`]; usermsg.val(message); usermsg.get(0).setSelectionRange(message.length, message.length); toggleSmileys(); usermsg.focus(); }; } /** * Adds the smileys container to the chat */ function addSmileys() { const smileysContainer = document.createElement('div'); smileysContainer.id = 'smileysContainer'; for (let i = 1; i <= 21; i++) { const smileyContainer = document.createElement('div'); smileyContainer.id = `smiley${i}`; smileyContainer.className = 'smileyContainer'; const smiley = document.createElement('img'); smiley.src = `images/smileys/smiley${i}.svg`; smiley.className = 'smiley'; addClickFunction(smiley, i); smileyContainer.appendChild(smiley); smileysContainer.appendChild(smileyContainer); } $('#chat_container').append(smileysContainer); } /** * Resizes the chat conversation. */ function resizeChatConversation() { // FIXME: this function can all be done with CSS. If Chat is ever rewritten, // do not copy over this logic. const msgareaHeight = $('#usermsg').outerHeight(); const chatspace = $(`#${CHAT_CONTAINER_ID}`); const width = chatspace.width(); const chat = $('#chatconversation'); const smileys = $('#smileysarea'); // eslint-disable-line no-shadow smileys.height(msgareaHeight); $('#smileys').css('bottom', (msgareaHeight - 26) / 2); $('#smileysContainer').css('bottom', msgareaHeight); chat.width(width - 10); const maybeAMagicNumberForPaddingAndMargin = 100; const offset = maybeAMagicNumberForPaddingAndMargin + msgareaHeight + getToolboxHeight(); chat.height(window.innerHeight - offset); } /** * Focus input after 400 ms * Found input by id * * @param id {string} input id */ function deferredFocus(id) { setTimeout(() => $(`#${id}`).focus(), 400); } /** * Chat related user interface. */ const Chat = { /** * Initializes chat related interface. */ init(eventEmitter) { initHTML(); if (APP.conference.getLocalDisplayName()) { Chat.setChatConversationMode(true); } $('#smileys').click(() => { Chat.toggleSmileys(); }); $('#nickinput').keydown(function(event) { if (event.keyCode === 13) { event.preventDefault(); const val = this.value; // eslint-disable-line no-invalid-this this.value = '';// eslint-disable-line no-invalid-this eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val); deferredFocus('usermsg'); } }); const usermsg = $('#usermsg'); usermsg.keydown(function(event) { if (event.keyCode === 13) { event.preventDefault(); const value = this.value; // eslint-disable-line no-invalid-this usermsg.val('').trigger('autosize.resize'); this.focus();// eslint-disable-line no-invalid-this const message = UIUtil.escapeHtml(value); eventEmitter.emit(UIEvents.MESSAGE_CREATED, message); } }); const onTextAreaResize = function() { resizeChatConversation(); Chat.scrollChatToBottom(); }; usermsg.autosize({ callback: onTextAreaResize }); eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED, (containerId, isVisible) => { if (containerId !== CHAT_CONTAINER_ID || !isVisible) { return; } unreadMessages = 0; APP.store.dispatch(markAllRead()); updateVisualNotification(); // Undock the toolbar when the chat is shown and if we're in a // video mode. if (VideoLayout.isLargeVideoVisible()) { APP.store.dispatch(dockToolbox(false)); } // if we are in conversation mode focus on the text input // if we are not, focus on the display name input deferredFocus( APP.conference.getLocalDisplayName() ? 'usermsg' : 'nickinput'); }); addSmileys(); updateVisualNotification(); }, /** * Appends the given message to the chat conversation. */ // eslint-disable-next-line max-params updateChatConversation(id, displayName, message, stamp) { const isFromLocalParticipant = APP.conference.isLocalId(id); let divClassName = ''; if (isFromLocalParticipant) { divClassName = 'localuser'; } else { divClassName = 'remoteuser'; if (!Chat.isVisible()) { unreadMessages++; updateVisualNotification(); } } // replace links and smileys // Strophe already escapes special symbols on sending, // so we escape here only tags to avoid double & const escMessage = message.replace(//g, '>') .replace(/\n/g, '
'); const escDisplayName = UIUtil.escapeHtml(displayName); const timestamp = getCurrentTime(stamp); // eslint-disable-next-line no-param-reassign message = processReplacements(escMessage); const messageContainer = `${'
' + '' + '
${escDisplayName }
${timestamp }
${message}
` + '
'; $('#chatconversation').append(messageContainer); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight }, 1000); const markAsRead = Chat.isVisible() || isFromLocalParticipant; APP.store.dispatch(addMessage( escDisplayName, message, timestamp, markAsRead)); }, /** * Appends error message to the conversation * @param errorMessage the received error message. * @param originalText the original message. */ chatAddError(errorMessage, originalText) { // eslint-disable-next-line no-param-reassign errorMessage = UIUtil.escapeHtml(errorMessage); // eslint-disable-next-line no-param-reassign originalText = UIUtil.escapeHtml(originalText); $('#chatconversation').append( `${'
Error: Your message'}${ originalText ? ` "${originalText}"` : '' } was not sent.${ errorMessage ? ` Reason: ${errorMessage}` : ''}
`); $('#chatconversation').animate( { scrollTop: $('#chatconversation')[0].scrollHeight }, 1000); }, /** * Sets the chat conversation mode. * Conversation mode is the normal chat mode, non conversation mode is * where we ask user to input its display name. * @param {boolean} isConversationMode if chat should be in * conversation mode or not. */ setChatConversationMode(isConversationMode) { $(`#${CHAT_CONTAINER_ID}`) .toggleClass('is-conversation-mode', isConversationMode); }, /** * Resizes the chat area. */ resizeChat(width, height) { $(`#${CHAT_CONTAINER_ID}`).width(width) .height(height); resizeChatConversation(); }, /** * Indicates if the chat is currently visible. */ isVisible() { return UIUtil.isVisible( document.getElementById(CHAT_CONTAINER_ID)); }, /** * Shows and hides the window with the smileys */ toggleSmileys, /** * Scrolls chat to the bottom. */ scrollChatToBottom() { setTimeout( () => { const chatconversation = $('#chatconversation'); // XXX Prevent TypeError: undefined is not an object when the // Web browser does not support WebRTC (yet). chatconversation.length > 0 && chatconversation.scrollTop( chatconversation[0].scrollHeight); }, 5); } }; export default Chat;