選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

Chat.js 13KB

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