You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Chat.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. /* global APP, $ */
  2. import { processReplacements, linkify } from './Replacement';
  3. import CommandsProcessor from './Commands';
  4. import VideoLayout from '../../videolayout/VideoLayout';
  5. import UIUtil from '../../util/UIUtil';
  6. import UIEvents from '../../../../service/UI/UIEvents';
  7. import { smileys } from './smileys';
  8. import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
  9. let unreadMessages = 0;
  10. const sidePanelsContainerId = 'sideToolbarContainer';
  11. const htmlStr = `
  12. <div id="chat_container" class="sideToolbarContainer__inner">
  13. <div id="nickname">
  14. <span data-i18n="chat.nickname.title"></span>
  15. <form>
  16. <input type='text'
  17. class="input-control" id="nickinput" autofocus
  18. data-i18n="[placeholder]chat.nickname.popover">
  19. </form>
  20. </div>
  21. <div id="chatconversation"></div>
  22. <audio id="chatNotification" src="sounds/incomingMessage.wav"
  23. preload="auto"></audio>
  24. <textarea id="usermsg" autofocus
  25. data-i18n="[placeholder]chat.messagebox"></textarea>
  26. <div id="smileysarea">
  27. <div id="smileys">
  28. <img src="images/smile.svg"/>
  29. </div>
  30. </div>
  31. </div>`;
  32. /**
  33. *
  34. */
  35. function initHTML() {
  36. $(`#${sidePanelsContainerId}`)
  37. .append(htmlStr);
  38. }
  39. /**
  40. * The container id, which is and the element id.
  41. */
  42. const CHAT_CONTAINER_ID = 'chat_container';
  43. /**
  44. * Updates visual notification, indicating that a message has arrived.
  45. */
  46. function updateVisualNotification() {
  47. // XXX The rewrite of the toolbar in React delayed the availability of the
  48. // element unreadMessages. In order to work around the delay, I introduced
  49. // and utilized unreadMsgSelector in addition to unreadMsgElement.
  50. const unreadMsgSelector = $('#unreadMessages');
  51. const unreadMsgElement
  52. = unreadMsgSelector.length > 0 ? unreadMsgSelector[0] : undefined;
  53. if (unreadMessages && unreadMsgElement) {
  54. unreadMsgElement.innerHTML = unreadMessages.toString();
  55. APP.store.dispatch(dockToolbox(true));
  56. const chatButtonElement
  57. = document.getElementById('toolbar_button_chat');
  58. const leftIndent
  59. = (UIUtil.getTextWidth(chatButtonElement)
  60. - UIUtil.getTextWidth(unreadMsgElement)) / 2;
  61. const topIndent
  62. = ((UIUtil.getTextHeight(chatButtonElement)
  63. - UIUtil.getTextHeight(unreadMsgElement)) / 2) - 5;
  64. unreadMsgElement.setAttribute(
  65. 'style',
  66. `top:${topIndent}; left:${leftIndent};`);
  67. } else {
  68. unreadMsgSelector.html('');
  69. }
  70. if (unreadMsgElement) {
  71. unreadMsgSelector.parent()[unreadMessages > 0 ? 'show' : 'hide']();
  72. }
  73. }
  74. /**
  75. * Returns the current time in the format it is shown to the user
  76. * @returns {string}
  77. */
  78. function getCurrentTime(stamp) {
  79. const now = stamp ? new Date(stamp) : new Date();
  80. let hour = now.getHours();
  81. let minute = now.getMinutes();
  82. let second = now.getSeconds();
  83. if (hour.toString().length === 1) {
  84. hour = `0${hour}`;
  85. }
  86. if (minute.toString().length === 1) {
  87. minute = `0${minute}`;
  88. }
  89. if (second.toString().length === 1) {
  90. second = `0${second}`;
  91. }
  92. return `${hour}:${minute}:${second}`;
  93. }
  94. /**
  95. *
  96. */
  97. function toggleSmileys() {
  98. const smileys = $('#smileysContainer'); // eslint-disable-line no-shadow
  99. if (smileys.is(':visible')) {
  100. smileys.hide('slide', { direction: 'down',
  101. duration: 300 });
  102. } else {
  103. smileys.show('slide', { direction: 'down',
  104. duration: 300 });
  105. }
  106. $('#usermsg').focus();
  107. }
  108. /**
  109. *
  110. */
  111. function addClickFunction(smiley, number) {
  112. smiley.onclick = function addSmileyToMessage() {
  113. const usermsg = $('#usermsg');
  114. let message = usermsg.val();
  115. message += smileys[`smiley${number}`];
  116. usermsg.val(message);
  117. usermsg.get(0).setSelectionRange(message.length, message.length);
  118. toggleSmileys();
  119. usermsg.focus();
  120. };
  121. }
  122. /**
  123. * Adds the smileys container to the chat
  124. */
  125. function addSmileys() {
  126. const smileysContainer = document.createElement('div');
  127. smileysContainer.id = 'smileysContainer';
  128. for (let i = 1; i <= 21; i++) {
  129. const smileyContainer = document.createElement('div');
  130. smileyContainer.id = `smiley${i}`;
  131. smileyContainer.className = 'smileyContainer';
  132. const smiley = document.createElement('img');
  133. smiley.src = `images/smileys/smiley${i}.svg`;
  134. smiley.className = 'smiley';
  135. addClickFunction(smiley, i);
  136. smileyContainer.appendChild(smiley);
  137. smileysContainer.appendChild(smileyContainer);
  138. }
  139. $('#chat_container').append(smileysContainer);
  140. }
  141. /**
  142. * Resizes the chat conversation.
  143. */
  144. function resizeChatConversation() {
  145. const msgareaHeight = $('#usermsg').outerHeight();
  146. const chatspace = $(`#${CHAT_CONTAINER_ID}`);
  147. const width = chatspace.width();
  148. const chat = $('#chatconversation');
  149. const smileys = $('#smileysarea'); // eslint-disable-line no-shadow
  150. smileys.height(msgareaHeight);
  151. $('#smileys').css('bottom', (msgareaHeight - 26) / 2);
  152. $('#smileysContainer').css('bottom', msgareaHeight);
  153. chat.width(width - 10);
  154. chat.height(window.innerHeight - 15 - msgareaHeight);
  155. }
  156. /**
  157. * Focus input after 400 ms
  158. * Found input by id
  159. *
  160. * @param id {string} input id
  161. */
  162. function deferredFocus(id) {
  163. setTimeout(() => $(`#${id}`).focus(), 400);
  164. }
  165. /**
  166. * Chat related user interface.
  167. */
  168. const Chat = {
  169. /**
  170. * Initializes chat related interface.
  171. */
  172. init(eventEmitter) {
  173. initHTML();
  174. if (APP.conference.getLocalDisplayName()) {
  175. Chat.setChatConversationMode(true);
  176. }
  177. $('#smileys').click(() => {
  178. Chat.toggleSmileys();
  179. });
  180. $('#nickinput').keydown(function(event) {
  181. if (event.keyCode === 13) {
  182. event.preventDefault();
  183. const val = this.value; // eslint-disable-line no-invalid-this
  184. this.value = '';// eslint-disable-line no-invalid-this
  185. eventEmitter.emit(UIEvents.NICKNAME_CHANGED, val);
  186. deferredFocus('usermsg');
  187. }
  188. });
  189. const usermsg = $('#usermsg');
  190. usermsg.keydown(function(event) {
  191. if (event.keyCode === 13) {
  192. event.preventDefault();
  193. const value = this.value; // eslint-disable-line no-invalid-this
  194. usermsg.val('').trigger('autosize.resize');
  195. this.focus();// eslint-disable-line no-invalid-this
  196. const command = new CommandsProcessor(value, eventEmitter);
  197. if (command.isCommand()) {
  198. command.processCommand();
  199. } else {
  200. const message = UIUtil.escapeHtml(value);
  201. eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
  202. }
  203. }
  204. });
  205. const onTextAreaResize = function() {
  206. resizeChatConversation();
  207. Chat.scrollChatToBottom();
  208. };
  209. usermsg.autosize({ callback: onTextAreaResize });
  210. eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
  211. (containerId, isVisible) => {
  212. if (containerId !== CHAT_CONTAINER_ID || !isVisible) {
  213. return;
  214. }
  215. unreadMessages = 0;
  216. updateVisualNotification();
  217. // Undock the toolbar when the chat is shown and if we're in a
  218. // video mode.
  219. if (VideoLayout.isLargeVideoVisible()) {
  220. APP.store.dispatch(dockToolbox(false));
  221. }
  222. // if we are in conversation mode focus on the text input
  223. // if we are not, focus on the display name input
  224. deferredFocus(
  225. APP.conference.getLocalDisplayName()
  226. ? 'usermsg'
  227. : 'nickinput');
  228. });
  229. addSmileys();
  230. updateVisualNotification();
  231. },
  232. /**
  233. * Appends the given message to the chat conversation.
  234. */
  235. // eslint-disable-next-line max-params
  236. updateChatConversation(id, displayName, message, stamp) {
  237. let divClassName = '';
  238. if (APP.conference.isLocalId(id)) {
  239. divClassName = 'localuser';
  240. } else {
  241. divClassName = 'remoteuser';
  242. if (!Chat.isVisible()) {
  243. unreadMessages++;
  244. UIUtil.playSoundNotification('chatNotification');
  245. updateVisualNotification();
  246. }
  247. }
  248. // replace links and smileys
  249. // Strophe already escapes special symbols on sending,
  250. // so we escape here only tags to avoid double &amp;
  251. const escMessage = message.replace(/</g, '&lt;')
  252. .replace(/>/g, '&gt;')
  253. .replace(/\n/g, '<br/>');
  254. const escDisplayName = UIUtil.escapeHtml(displayName);
  255. // eslint-disable-next-line no-param-reassign
  256. message = processReplacements(escMessage);
  257. const messageContainer
  258. = `${'<div class="chatmessage">'
  259. + '<img src="images/chatArrow.svg" class="chatArrow">'
  260. + '<div class="username '}${divClassName}">${escDisplayName
  261. }</div><div class="timestamp">${getCurrentTime(stamp)
  262. }</div><div class="usermessage">${message}</div>`
  263. + '</div>';
  264. $('#chatconversation').append(messageContainer);
  265. $('#chatconversation').animate(
  266. { scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
  267. },
  268. /**
  269. * Appends error message to the conversation
  270. * @param errorMessage the received error message.
  271. * @param originalText the original message.
  272. */
  273. chatAddError(errorMessage, originalText) {
  274. // eslint-disable-next-line no-param-reassign
  275. errorMessage = UIUtil.escapeHtml(errorMessage);
  276. // eslint-disable-next-line no-param-reassign
  277. originalText = UIUtil.escapeHtml(originalText);
  278. $('#chatconversation').append(
  279. `${'<div class="errorMessage"><b>Error: </b>Your message'}${
  280. originalText ? ` "${originalText}"` : ''
  281. } was not sent.${
  282. errorMessage ? ` Reason: ${errorMessage}` : ''}</div>`);
  283. $('#chatconversation').animate(
  284. { scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
  285. },
  286. /**
  287. * Sets the subject to the UI
  288. * @param subject the subject
  289. */
  290. setSubject(subject) {
  291. if (subject) {
  292. // eslint-disable-next-line no-param-reassign
  293. subject = subject.trim();
  294. }
  295. const html = linkify(UIUtil.escapeHtml(subject));
  296. APP.store.dispatch(setSubject(html));
  297. },
  298. /**
  299. * Sets the chat conversation mode.
  300. * Conversation mode is the normal chat mode, non conversation mode is
  301. * where we ask user to input its display name.
  302. * @param {boolean} isConversationMode if chat should be in
  303. * conversation mode or not.
  304. */
  305. setChatConversationMode(isConversationMode) {
  306. $(`#${CHAT_CONTAINER_ID}`)
  307. .toggleClass('is-conversation-mode', isConversationMode);
  308. },
  309. /**
  310. * Resizes the chat area.
  311. */
  312. resizeChat(width, height) {
  313. $(`#${CHAT_CONTAINER_ID}`).width(width)
  314. .height(height);
  315. resizeChatConversation();
  316. },
  317. /**
  318. * Indicates if the chat is currently visible.
  319. */
  320. isVisible() {
  321. return UIUtil.isVisible(
  322. document.getElementById(CHAT_CONTAINER_ID));
  323. },
  324. /**
  325. * Shows and hides the window with the smileys
  326. */
  327. toggleSmileys,
  328. /**
  329. * Scrolls chat to the bottom.
  330. */
  331. scrollChatToBottom() {
  332. setTimeout(
  333. () => {
  334. const chatconversation = $('#chatconversation');
  335. // XXX Prevent TypeError: undefined is not an object when the
  336. // Web browser does not support WebRTC (yet).
  337. chatconversation.length > 0
  338. && chatconversation.scrollTop(
  339. chatconversation[0].scrollHeight);
  340. },
  341. 5);
  342. }
  343. };
  344. export default Chat;