423 lines
12 KiB
JavaScript
423 lines
12 KiB
JavaScript
/* global APP, $ */
|
|
|
|
import { processReplacements, linkify } from './Replacement';
|
|
import CommandsProcessor from './Commands';
|
|
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,
|
|
setSubject
|
|
} from '../../../../react/features/toolbox';
|
|
|
|
let unreadMessages = 0;
|
|
const sidePanelsContainerId = 'sideToolbarContainer';
|
|
const htmlStr = `
|
|
<div id="chat_container" class="sideToolbarContainer__inner">
|
|
<div id="nickname">
|
|
<span data-i18n="chat.nickname.title"></span>
|
|
<form>
|
|
<input type='text'
|
|
class="input-control" id="nickinput" autofocus
|
|
data-i18n="[placeholder]chat.nickname.popover">
|
|
</form>
|
|
</div>
|
|
|
|
<div id="chatconversation"></div>
|
|
<textarea id="usermsg" autofocus
|
|
data-i18n="[placeholder]chat.messagebox"></textarea>
|
|
<div id="smileysarea">
|
|
<div id="smileys">
|
|
<img src="images/smile.svg"/>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function initHTML() {
|
|
$(`#${sidePanelsContainerId}`)
|
|
.append(htmlStr);
|
|
}
|
|
|
|
/**
|
|
* 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 command = new CommandsProcessor(value, eventEmitter);
|
|
|
|
if (command.isCommand()) {
|
|
command.processCommand();
|
|
} else {
|
|
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(/>/g, '>')
|
|
.replace(/\n/g, '<br/>');
|
|
const escDisplayName = UIUtil.escapeHtml(displayName);
|
|
const timestamp = getCurrentTime(stamp);
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
message = processReplacements(escMessage);
|
|
|
|
const messageContainer
|
|
= `${'<div class="chatmessage">'
|
|
+ '<img src="images/chatArrow.svg" class="chatArrow">'
|
|
+ '<div class="username '}${divClassName}">${escDisplayName
|
|
}</div><div class="timestamp">${timestamp
|
|
}</div><div class="usermessage">${message}</div>`
|
|
+ '</div>';
|
|
|
|
$('#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(
|
|
`${'<div class="errorMessage"><b>Error: </b>Your message'}${
|
|
originalText ? ` "${originalText}"` : ''
|
|
} was not sent.${
|
|
errorMessage ? ` Reason: ${errorMessage}` : ''}</div>`);
|
|
$('#chatconversation').animate(
|
|
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
|
},
|
|
|
|
/**
|
|
* Sets the subject to the UI
|
|
* @param subject the subject
|
|
*/
|
|
setSubject(subject) {
|
|
if (subject) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
subject = subject.trim();
|
|
}
|
|
|
|
const html = linkify(UIUtil.escapeHtml(subject));
|
|
|
|
APP.store.dispatch(setSubject(html));
|
|
},
|
|
|
|
/**
|
|
* 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;
|