Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

Chat.js 12KB

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