message/amd/src/message_drawer_view_conversation.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Controls the conversation page in the message drawer.
  17. *
  18. * This function handles all of the user actions that the user can take
  19. * when interacting with the conversation page.
  20. *
  21. * It maintains a view state which is a data representation of the view
  22. * and only operates on that data.
  23. *
  24. * The view state is immutable and should never be modified directly. Instead
  25. * all changes to the view state should be done using the StateManager which
  26. * will generate a new version of the view state with the requested changes.
  27. *
  28. * After any changes to the view state the module will call the render function
  29. * to ask the renderer to update the UI.
  30. *
  31. * General rules for this module:
  32. * 1.) Never modify viewState directly. All changes should be via the StateManager.
  33. * 2.) Call render() with the new state when you want to update the UI
  34. * 3.) Never modify the UI directly in this module. This module is only concerned
  35. * with the data in the view state.
  36. *
  37. * The general flow for a user interaction will be something like:
  38. * User interaction: User clicks "confirm block" button to block the other user
  39. * 1.) This module is hears the click
  40. * 2.) This module sends a request to the server to block the user
  41. * 3.) The server responds with the new user profile
  42. * 4.) This module generates a new state using the StateManager with the updated
  43. * user profile.
  44. * 5.) This module asks the Patcher to generate a patch from the current state and
  45. * the newly generated state. This patch tells the renderer what has changed
  46. * between the states.
  47. * 6.) This module gives the Renderer the generated patch. The renderer updates
  48. * the UI with changes according to the patch.
  49. *
  50. * @module core_message/message_drawer_view_conversation
  51. * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
  52. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  53. */
  54. define(
  55. [
  56. 'jquery',
  57. 'core/auto_rows',
  58. 'core/backoff_timer',
  59. 'core/custom_interaction_events',
  60. 'core/notification',
  61. 'core/pending',
  62. 'core/pubsub',
  63. 'core/str',
  64. 'core_message/message_repository',
  65. 'core_message/message_drawer_events',
  66. 'core_message/message_drawer_view_conversation_constants',
  67. 'core_message/message_drawer_view_conversation_patcher',
  68. 'core_message/message_drawer_view_conversation_renderer',
  69. 'core_message/message_drawer_view_conversation_state_manager',
  70. 'core_message/message_drawer_router',
  71. 'core_message/message_drawer_routes',
  72. 'core/emoji/auto_complete',
  73. 'core/emoji/picker'
  74. ],
  75. function(
  76. $,
  77. AutoRows,
  78. BackOffTimer,
  79. CustomEvents,
  80. Notification,
  81. Pending,
  82. PubSub,
  83. Str,
  84. Repository,
  85. MessageDrawerEvents,
  86. Constants,
  87. Patcher,
  88. Renderer,
  89. StateManager,
  90. MessageDrawerRouter,
  91. MessageDrawerRoutes,
  92. initialiseEmojiAutoComplete,
  93. initialiseEmojiPicker
  94. ) {
  95. // Contains a cache of all view states that have been loaded so far
  96. // which saves us having to reload stuff with network requests when
  97. // switching between conversations.
  98. var stateCache = {};
  99. // The current data representation of the view.
  100. var viewState = null;
  101. var loadedAllMessages = false;
  102. var messagesOffset = 0;
  103. var newMessagesPollTimer = null;
  104. var isRendering = false;
  105. var renderBuffer = [];
  106. // If the UI is currently resetting.
  107. var isResetting = true;
  108. // If the UI is currently sending a message.
  109. var isSendingMessage = false;
  110. // If the UI is currently deleting a conversation.
  111. var isDeletingConversationContent = false;
  112. // A buffer of messages to send.
  113. var sendMessageBuffer = [];
  114. // These functions which will be generated when this module is
  115. // first called. See generateRenderFunction for details.
  116. var render = null;
  117. // The list of renderers that have been registered to render
  118. // this conversation. See generateRenderFunction for details.
  119. var renderers = [];
  120. var NEWEST_FIRST = Constants.NEWEST_MESSAGES_FIRST;
  121. var LOAD_MESSAGE_LIMIT = Constants.LOAD_MESSAGE_LIMIT;
  122. var MILLISECONDS_IN_SEC = Constants.MILLISECONDS_IN_SEC;
  123. var SELECTORS = Constants.SELECTORS;
  124. var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
  125. /**
  126. * Get the other user userid.
  127. *
  128. * @return {Number} Userid.
  129. */
  130. var getOtherUserId = function() {
  131. if (!viewState || viewState.type == CONVERSATION_TYPES.PUBLIC) {
  132. return null;
  133. }
  134. var loggedInUserId = viewState.loggedInUserId;
  135. if (viewState.type == CONVERSATION_TYPES.SELF) {
  136. // It's a self-conversation, so the other user is the one logged in.
  137. return loggedInUserId;
  138. }
  139. var otherUserIds = Object.keys(viewState.members).filter(function(userId) {
  140. return loggedInUserId != userId;
  141. });
  142. return otherUserIds.length ? otherUserIds[0] : null;
  143. };
  144. /**
  145. * Search the cache to see if we've already loaded a private conversation
  146. * with the given user id.
  147. *
  148. * @param {Number} userId The id of the other user.
  149. * @return {Number|null} Conversation id.
  150. */
  151. var getCachedPrivateConversationIdFromUserId = function(userId) {
  152. return Object.keys(stateCache).reduce(function(carry, id) {
  153. if (!carry) {
  154. var state = stateCache[id].state;
  155. if (state.type != CONVERSATION_TYPES.PUBLIC) {
  156. if (userId in state.members) {
  157. // We've found a cached conversation for this user!
  158. carry = state.id;
  159. }
  160. }
  161. }
  162. return carry;
  163. }, null);
  164. };
  165. /**
  166. * Get profile info for logged in user.
  167. *
  168. * @param {Object} body Conversation body container element.
  169. * @return {Object}
  170. */
  171. var getLoggedInUserProfile = function(body) {
  172. return {
  173. id: parseInt(body.attr('data-user-id'), 10),
  174. fullname: null,
  175. profileimageurl: null,
  176. profileimageurlsmall: null,
  177. isonline: null,
  178. showonlinestatus: null,
  179. isblocked: null,
  180. iscontact: null,
  181. isdeleted: null,
  182. canmessage: null,
  183. canmessageevenifblocked: null,
  184. requirescontact: null,
  185. contactrequests: []
  186. };
  187. };
  188. /**
  189. * Get the messages offset value to load more messages.
  190. *
  191. * @return {Number}
  192. */
  193. var getMessagesOffset = function() {
  194. return messagesOffset;
  195. };
  196. /**
  197. * Set the messages offset value for loading more messages.
  198. *
  199. * @param {Number} value The offset value
  200. */
  201. var setMessagesOffset = function(value) {
  202. messagesOffset = value;
  203. stateCache[viewState.id].messagesOffset = value;
  204. };
  205. /**
  206. * Check if all messages have been loaded.
  207. *
  208. * @return {Bool}
  209. */
  210. var hasLoadedAllMessages = function() {
  211. return loadedAllMessages;
  212. };
  213. /**
  214. * Set whether all messages have been loaded or not.
  215. *
  216. * @param {Bool} value If all messages have been loaded.
  217. */
  218. var setLoadedAllMessages = function(value) {
  219. loadedAllMessages = value;
  220. stateCache[viewState.id].loadedAllMessages = value;
  221. };
  222. /**
  223. * Get the messages container element.
  224. *
  225. * @param {Object} body Conversation body container element.
  226. * @return {Object} The messages container element.
  227. */
  228. var getMessagesContainer = function(body) {
  229. return body.find(SELECTORS.MESSAGES_CONTAINER);
  230. };
  231. /**
  232. * Reformat the conversation for an event payload.
  233. *
  234. * @param {Object} state The view state.
  235. * @return {Object} New formatted conversation.
  236. */
  237. var formatConversationForEvent = function(state) {
  238. return {
  239. id: state.id,
  240. name: state.name,
  241. subname: state.subname,
  242. imageUrl: state.imageUrl,
  243. isFavourite: state.isFavourite,
  244. isMuted: state.isMuted,
  245. type: state.type,
  246. totalMemberCount: state.totalMemberCount,
  247. loggedInUserId: state.loggedInUserId,
  248. messages: state.messages.map(function(message) {
  249. return $.extend({}, message);
  250. }),
  251. members: Object.keys(state.members).map(function(id) {
  252. var formattedMember = $.extend({}, state.members[id]);
  253. formattedMember.contactrequests = state.members[id].contactrequests.map(function(request) {
  254. return $.extend({}, request);
  255. });
  256. return formattedMember;
  257. })
  258. };
  259. };
  260. /**
  261. * Load up an empty private conversation between the logged in user and the
  262. * other user. Sets all of the conversation details based on the other user.
  263. *
  264. * A conversation isn't created until the user sends the first message.
  265. *
  266. * @param {Object} loggedInUserProfile The logged in user profile.
  267. * @param {Number} otherUserId The other user id.
  268. * @return {Object} Profile returned from repository.
  269. */
  270. var loadEmptyPrivateConversation = function(loggedInUserProfile, otherUserId) {
  271. var loggedInUserId = loggedInUserProfile.id;
  272. // If the other user id is the same as the logged in user then this is a self
  273. // conversation.
  274. var conversationType = loggedInUserId == otherUserId ? CONVERSATION_TYPES.SELF : CONVERSATION_TYPES.PRIVATE;
  275. var newState = StateManager.setLoadingMembers(viewState, true);
  276. newState = StateManager.setLoadingMessages(newState, true);
  277. render(newState);
  278. return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true)
  279. .then(function(profiles) {
  280. if (profiles.length) {
  281. return profiles[0];
  282. } else {
  283. throw new Error('Unable to load other user profile');
  284. }
  285. })
  286. .then(function(profile) {
  287. // If the conversation is a self conversation then the profile loaded is the
  288. // logged in user so only add that to the members array.
  289. var members = conversationType == CONVERSATION_TYPES.SELF ? [profile] : [profile, loggedInUserProfile];
  290. var newState = StateManager.addMembers(viewState, members);
  291. newState = StateManager.setLoadingMembers(newState, false);
  292. newState = StateManager.setLoadingMessages(newState, false);
  293. newState = StateManager.setName(newState, profile.fullname);
  294. newState = StateManager.setType(newState, conversationType);
  295. newState = StateManager.setImageUrl(newState, profile.profileimageurl);
  296. newState = StateManager.setTotalMemberCount(newState, members.length);
  297. render(newState);
  298. return profile;
  299. })
  300. .catch(function(error) {
  301. var newState = StateManager.setLoadingMembers(viewState, false);
  302. render(newState);
  303. Notification.exception(error);
  304. });
  305. };
  306. /**
  307. * Create a new state from a conversation object.
  308. *
  309. * @param {Object} conversation The conversation object.
  310. * @param {Number} loggedInUserId The logged in user id.
  311. * @return {Object} new state.
  312. */
  313. var updateStateFromConversation = function(conversation, loggedInUserId) {
  314. var otherUser = null;
  315. if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
  316. // For private conversations, remove current logged in user from the members list to get the other user.
  317. var otherUsers = conversation.members.filter(function(member) {
  318. return member.id != loggedInUserId;
  319. });
  320. otherUser = otherUsers.length ? otherUsers[0] : null;
  321. } else if (conversation.type == CONVERSATION_TYPES.SELF) {
  322. // Self-conversations have only one member.
  323. otherUser = conversation.members[0];
  324. }
  325. var name = conversation.name;
  326. var imageUrl = conversation.imageurl;
  327. if (conversation.type != CONVERSATION_TYPES.PUBLIC) {
  328. name = name || otherUser ? otherUser.fullname : '';
  329. imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : '';
  330. }
  331. var newState = StateManager.addMembers(viewState, conversation.members);
  332. newState = StateManager.setName(newState, name);
  333. newState = StateManager.setSubname(newState, conversation.subname);
  334. newState = StateManager.setType(newState, conversation.type);
  335. newState = StateManager.setImageUrl(newState, imageUrl);
  336. newState = StateManager.setTotalMemberCount(newState, conversation.membercount);
  337. newState = StateManager.setIsFavourite(newState, conversation.isfavourite);
  338. newState = StateManager.setIsMuted(newState, conversation.ismuted);
  339. newState = StateManager.addMessages(newState, conversation.messages);
  340. newState = StateManager.setCanDeleteMessagesForAllUsers(newState, conversation.candeletemessagesforallusers);
  341. return newState;
  342. };
  343. /**
  344. * Get the details for a conversation from the conversation id.
  345. *
  346. * @param {Number} conversationId The conversation id.
  347. * @param {Object} loggedInUserProfile The logged in user profile.
  348. * @param {Number} messageLimit The number of messages to include.
  349. * @param {Number} messageOffset The number of messages to skip.
  350. * @param {Bool} newestFirst Order messages newest first.
  351. * @return {Object} Promise resolved when loaded.
  352. */
  353. var loadNewConversation = function(
  354. conversationId,
  355. loggedInUserProfile,
  356. messageLimit,
  357. messageOffset,
  358. newestFirst
  359. ) {
  360. var loggedInUserId = loggedInUserProfile.id;
  361. var newState = StateManager.setLoadingMembers(viewState, true);
  362. newState = StateManager.setLoadingMessages(newState, true);
  363. render(newState);
  364. return Repository.getConversation(
  365. loggedInUserId,
  366. conversationId,
  367. true,
  368. true,
  369. 0,
  370. 0,
  371. messageLimit + 1,
  372. messageOffset,
  373. newestFirst
  374. )
  375. .then(function(conversation) {
  376. if (conversation.messages.length > messageLimit) {
  377. conversation.messages = conversation.messages.slice(1);
  378. } else {
  379. setLoadedAllMessages(true);
  380. }
  381. setMessagesOffset(messageOffset + messageLimit);
  382. return conversation;
  383. })
  384. .then(function(conversation) {
  385. var hasLoggedInUser = conversation.members.filter(function(member) {
  386. return member.id == loggedInUserProfile.id;
  387. });
  388. if (hasLoggedInUser.length < 1) {
  389. conversation.members = conversation.members.concat([loggedInUserProfile]);
  390. }
  391. var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);
  392. newState = StateManager.setLoadingMembers(newState, false);
  393. newState = StateManager.setLoadingMessages(newState, false);
  394. return render(newState)
  395. .then(function() {
  396. return conversation;
  397. });
  398. })
  399. .then(function() {
  400. return markConversationAsRead(conversationId);
  401. })
  402. .catch(function(error) {
  403. var newState = StateManager.setLoadingMembers(viewState, false);
  404. newState = StateManager.setLoadingMessages(newState, false);
  405. render(newState);
  406. Notification.exception(error);
  407. });
  408. };
  409. /**
  410. * Get the details for a conversation from and existing conversation object.
  411. *
  412. * @param {Object} conversation The conversation object.
  413. * @param {Object} loggedInUserProfile The logged in user profile.
  414. * @param {Number} messageLimit The number of messages to include.
  415. * @param {Bool} newestFirst Order messages newest first.
  416. * @return {Object} Promise resolved when loaded.
  417. */
  418. var loadExistingConversation = function(
  419. conversation,
  420. loggedInUserProfile,
  421. messageLimit,
  422. newestFirst
  423. ) {
  424. var hasLoggedInUser = conversation.members.filter(function(member) {
  425. return member.id == loggedInUserProfile.id;
  426. });
  427. if (hasLoggedInUser.length < 1) {
  428. conversation.members = conversation.members.concat([loggedInUserProfile]);
  429. }
  430. var messageCount = conversation.messages.length;
  431. var hasLoadedEnoughMessages = messageCount >= messageLimit;
  432. var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);
  433. newState = StateManager.setLoadingMembers(newState, false);
  434. newState = StateManager.setLoadingMessages(newState, !hasLoadedEnoughMessages);
  435. var renderPromise = render(newState);
  436. return renderPromise.then(function() {
  437. if (!hasLoadedEnoughMessages) {
  438. // We haven't got enough messages so let's load some more.
  439. return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, []);
  440. } else {
  441. // We've got enough messages. No need to load any more for now.
  442. return {messages: conversation.messages};
  443. }
  444. })
  445. .then(function() {
  446. var messages = viewState.messages;
  447. // Update the offset to reflect the number of messages we've loaded.
  448. setMessagesOffset(messages.length);
  449. markConversationAsRead(viewState.id);
  450. return messages;
  451. })
  452. .catch(Notification.exception);
  453. };
  454. /**
  455. * Load messages for this conversation and pass them to the renderer.
  456. *
  457. * @param {Number} conversationId Conversation id.
  458. * @param {Number} limit Number of messages to load.
  459. * @param {Number} offset Get messages from offset.
  460. * @param {Bool} newestFirst Get newest messages first.
  461. * @param {Array} ignoreList Ignore any messages with ids in this list.
  462. * @param {Number|null} timeFrom Only get messages from this time onwards.
  463. * @return {Promise} renderer promise.
  464. */
  465. var loadMessages = function(conversationId, limit, offset, newestFirst, ignoreList, timeFrom) {
  466. return Repository.getMessages(
  467. viewState.loggedInUserId,
  468. conversationId,
  469. limit ? limit + 1 : limit,
  470. offset,
  471. newestFirst,
  472. timeFrom
  473. )
  474. .then(function(result) {
  475. // Prevent older requests from contaminating the current view.
  476. if (result.id != viewState.id) {
  477. result.messages = [];
  478. // Purge old conversation cache to prevent messages lose.
  479. if (result.id in stateCache) {
  480. delete stateCache[result.id];
  481. }
  482. }
  483. return result;
  484. })
  485. .then(function(result) {
  486. if (result.messages.length && ignoreList.length) {
  487. result.messages = result.messages.filter(function(message) {
  488. // Skip any messages in our ignore list.
  489. return ignoreList.indexOf(parseInt(message.id, 10)) < 0;
  490. });
  491. }
  492. return result;
  493. })
  494. .then(function(result) {
  495. if (!limit) {
  496. return result;
  497. } else if (result.messages.length > limit) {
  498. // Ignore the last result which was just to test if there are more
  499. // to load.
  500. result.messages = result.messages.slice(0, -1);
  501. } else {
  502. setLoadedAllMessages(true);
  503. }
  504. return result;
  505. })
  506. .then(function(result) {
  507. var membersToAdd = result.members.filter(function(member) {
  508. return !(member.id in viewState.members);
  509. });
  510. var newState = StateManager.addMembers(viewState, membersToAdd);
  511. newState = StateManager.addMessages(newState, result.messages);
  512. newState = StateManager.setLoadingMessages(newState, false);
  513. return render(newState)
  514. .then(function() {
  515. return result;
  516. });
  517. })
  518. .catch(function(error) {
  519. var newState = StateManager.setLoadingMessages(viewState, false);
  520. render(newState);
  521. // Re-throw the error for other error handlers.
  522. throw error;
  523. });
  524. };
  525. /**
  526. * Create a callback function for getting new messages for this conversation.
  527. *
  528. * @param {Number} conversationId Conversation id.
  529. * @param {Bool} newestFirst Show newest messages first
  530. * @return {Function} Callback function that returns a renderer promise.
  531. */
  532. var getLoadNewMessagesCallback = function(conversationId, newestFirst) {
  533. return function() {
  534. var messages = viewState.messages;
  535. var mostRecentMessage = messages.length ? messages[messages.length - 1] : null;
  536. var lastTimeCreated = mostRecentMessage ? mostRecentMessage.timeCreated : null;
  537. if (lastTimeCreated && !isResetting && !isSendingMessage && !isDeletingConversationContent) {
  538. // There may be multiple messages with the same time created value since
  539. // the accuracy is only down to the second. The server will include these
  540. // messages in the result (since it does a >= comparison on time from) so
  541. // we need to filter them back out of the result so that we're left only
  542. // with the new messages.
  543. var ignoreMessageIds = [];
  544. for (var i = messages.length - 1; i >= 0; i--) {
  545. var message = messages[i];
  546. if (message.timeCreated === lastTimeCreated) {
  547. ignoreMessageIds.push(message.id);
  548. } else {
  549. // Since the messages are ordered in ascending order of time created
  550. // we can break as soon as we hit a message with a different time created
  551. // because we know all other messages will have lower values.
  552. break;
  553. }
  554. }
  555. return loadMessages(
  556. conversationId,
  557. 0,
  558. 0,
  559. newestFirst,
  560. ignoreMessageIds,
  561. lastTimeCreated
  562. )
  563. .then(function(result) {
  564. if (result.messages.length) {
  565. // If we found some results then restart the polling timer
  566. // because the other user might be sending messages.
  567. newMessagesPollTimer.restart();
  568. // We've also got a new last message so publish that for other
  569. // components to update.
  570. var conversation = formatConversationForEvent(viewState);
  571. PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
  572. return markConversationAsRead(conversationId);
  573. } else {
  574. return result;
  575. }
  576. });
  577. }
  578. return $.Deferred().resolve().promise();
  579. };
  580. };
  581. /**
  582. * Mark a conversation as read.
  583. *
  584. * @param {Number} conversationId The conversation id.
  585. * @return {Promise} The renderer promise.
  586. */
  587. var markConversationAsRead = function(conversationId) {
  588. var loggedInUserId = viewState.loggedInUserId;
  589. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');
  590. return Repository.markAllConversationMessagesAsRead(loggedInUserId, conversationId)
  591. .then(function() {
  592. var newState = StateManager.markMessagesAsRead(viewState, viewState.messages);
  593. PubSub.publish(MessageDrawerEvents.CONVERSATION_READ, conversationId);
  594. return render(newState);
  595. })
  596. .then(function(result) {
  597. pendingPromise.resolve();
  598. return result;
  599. });
  600. };
  601. /**
  602. * Tell the statemanager there is request to block a user and run the renderer
  603. * to show the block user dialogue.
  604. *
  605. * @param {Number} userId User id.
  606. */
  607. var requestBlockUser = function(userId) {
  608. cancelRequest(userId);
  609. var newState = StateManager.addPendingBlockUsersById(viewState, [userId]);
  610. render(newState);
  611. };
  612. /**
  613. * Send the repository a request to block a user, update the statemanager and publish
  614. * a contact has been blocked.
  615. *
  616. * @param {Number} userId User id of user to block.
  617. * @return {Promise} Renderer promise.
  618. */
  619. var blockUser = function(userId) {
  620. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  621. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:blockUser');
  622. render(newState);
  623. return Repository.blockUser(viewState.loggedInUserId, userId)
  624. .then(function(profile) {
  625. var newState = StateManager.addMembers(viewState, [profile]);
  626. newState = StateManager.removePendingBlockUsersById(newState, [userId]);
  627. newState = StateManager.setLoadingConfirmAction(newState, false);
  628. PubSub.publish(MessageDrawerEvents.CONTACT_BLOCKED, userId);
  629. return render(newState);
  630. })
  631. .then(function(result) {
  632. pendingPromise.resolve();
  633. return result;
  634. });
  635. };
  636. /**
  637. * Tell the statemanager there is a request to unblock a user and run the renderer
  638. * to show the unblock user dialogue.
  639. *
  640. * @param {Number} userId User id of user to unblock.
  641. */
  642. var requestUnblockUser = function(userId) {
  643. cancelRequest(userId);
  644. var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]);
  645. render(newState);
  646. };
  647. /**
  648. * Send the repository a request to unblock a user, update the statemanager and publish
  649. * a contact has been unblocked.
  650. *
  651. * @param {Number} userId User id of user to unblock.
  652. * @return {Promise} Renderer promise.
  653. */
  654. var unblockUser = function(userId) {
  655. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  656. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:unblockUser');
  657. render(newState);
  658. return Repository.unblockUser(viewState.loggedInUserId, userId)
  659. .then(function(profile) {
  660. var newState = StateManager.addMembers(viewState, [profile]);
  661. newState = StateManager.removePendingUnblockUsersById(newState, [userId]);
  662. newState = StateManager.setLoadingConfirmAction(newState, false);
  663. PubSub.publish(MessageDrawerEvents.CONTACT_UNBLOCKED, userId);
  664. return render(newState);
  665. })
  666. .then(function(result) {
  667. pendingPromise.resolve();
  668. return result;
  669. });
  670. };
  671. /**
  672. * Tell the statemanager there is a request to remove a user from the contact list
  673. * and run the renderer to show the remove user from contacts dialogue.
  674. *
  675. * @param {Number} userId User id of user to remove from contacts.
  676. */
  677. var requestRemoveContact = function(userId) {
  678. cancelRequest(userId);
  679. var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]);
  680. render(newState);
  681. };
  682. /**
  683. * Send the repository a request to remove a user from the contacts list. update the statemanager
  684. * and publish a contact has been removed.
  685. *
  686. * @param {Number} userId User id of user to remove from contacts.
  687. * @return {Promise} Renderer promise.
  688. */
  689. var removeContact = function(userId) {
  690. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  691. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:removeContact');
  692. render(newState);
  693. return Repository.deleteContacts(viewState.loggedInUserId, [userId])
  694. .then(function(profiles) {
  695. var newState = StateManager.addMembers(viewState, profiles);
  696. newState = StateManager.removePendingRemoveContactsById(newState, [userId]);
  697. newState = StateManager.setLoadingConfirmAction(newState, false);
  698. PubSub.publish(MessageDrawerEvents.CONTACT_REMOVED, userId);
  699. return render(newState);
  700. })
  701. .then(function(result) {
  702. pendingPromise.resolve();
  703. return result;
  704. });
  705. };
  706. /**
  707. * Tell the statemanager there is a request to add a user to the contact list
  708. * and run the renderer to show the add user to contacts dialogue.
  709. *
  710. * @param {Number} userId User id of user to add to contacts.
  711. */
  712. var requestAddContact = function(userId) {
  713. cancelRequest(userId);
  714. var newState = StateManager.addPendingAddContactsById(viewState, [userId]);
  715. render(newState);
  716. };
  717. /**
  718. * Send the repository a request to add a user to the contacts list. update the statemanager
  719. * and publish a contact has been added.
  720. *
  721. * @param {Number} userId User id of user to add to contacts.
  722. * @return {Promise} Renderer promise.
  723. */
  724. var addContact = function(userId) {
  725. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  726. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:addContactRequests');
  727. render(newState);
  728. return Repository.createContactRequest(viewState.loggedInUserId, userId)
  729. .then(function(response) {
  730. if (!response.request) {
  731. throw new Error(response.warnings[0].message);
  732. }
  733. return response.request;
  734. })
  735. .then(function(request) {
  736. var newState = StateManager.removePendingAddContactsById(viewState, [userId]);
  737. newState = StateManager.addContactRequests(newState, [request]);
  738. newState = StateManager.setLoadingConfirmAction(newState, false);
  739. return render(newState);
  740. })
  741. .then(function(result) {
  742. pendingPromise.resolve();
  743. return result;
  744. });
  745. };
  746. /**
  747. * Set the current conversation as a favourite conversation.
  748. *
  749. * @return {Promise} Renderer promise.
  750. */
  751. var setFavourite = function() {
  752. var userId = viewState.loggedInUserId;
  753. var conversationId = viewState.id;
  754. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:setFavourite');
  755. return Repository.setFavouriteConversations(userId, [conversationId])
  756. .then(function() {
  757. var newState = StateManager.setIsFavourite(viewState, true);
  758. return render(newState);
  759. })
  760. .then(function() {
  761. return PubSub.publish(
  762. MessageDrawerEvents.CONVERSATION_SET_FAVOURITE,
  763. formatConversationForEvent(viewState)
  764. );
  765. })
  766. .then(function(result) {
  767. pendingPromise.resolve();
  768. return result;
  769. });
  770. };
  771. /**
  772. * Unset the current conversation as a favourite conversation.
  773. *
  774. * @return {Promise} Renderer promise.
  775. */
  776. var unsetFavourite = function() {
  777. var userId = viewState.loggedInUserId;
  778. var conversationId = viewState.id;
  779. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:unsetFavourite');
  780. return Repository.unsetFavouriteConversations(userId, [conversationId])
  781. .then(function() {
  782. var newState = StateManager.setIsFavourite(viewState, false);
  783. return render(newState);
  784. })
  785. .then(function() {
  786. return PubSub.publish(
  787. MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE,
  788. formatConversationForEvent(viewState)
  789. );
  790. })
  791. .then(function(result) {
  792. pendingPromise.resolve();
  793. return result;
  794. });
  795. };
  796. /**
  797. * Set the current conversation as a muted conversation.
  798. *
  799. * @return {Promise} Renderer promise.
  800. */
  801. var setMuted = function() {
  802. var userId = viewState.loggedInUserId;
  803. var conversationId = viewState.id;
  804. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');
  805. return Repository.setMutedConversations(userId, [conversationId])
  806. .then(function() {
  807. var newState = StateManager.setIsMuted(viewState, true);
  808. return render(newState);
  809. })
  810. .then(function() {
  811. return PubSub.publish(
  812. MessageDrawerEvents.CONVERSATION_SET_MUTED,
  813. formatConversationForEvent(viewState)
  814. );
  815. })
  816. .then(function(result) {
  817. pendingPromise.resolve();
  818. return result;
  819. });
  820. };
  821. /**
  822. * Unset the current conversation as a muted conversation.
  823. *
  824. * @return {Promise} Renderer promise.
  825. */
  826. var unsetMuted = function() {
  827. var userId = viewState.loggedInUserId;
  828. var conversationId = viewState.id;
  829. return Repository.unsetMutedConversations(userId, [conversationId])
  830. .then(function() {
  831. var newState = StateManager.setIsMuted(viewState, false);
  832. return render(newState);
  833. })
  834. .then(function() {
  835. return PubSub.publish(
  836. MessageDrawerEvents.CONVERSATION_UNSET_MUTED,
  837. formatConversationForEvent(viewState)
  838. );
  839. });
  840. };
  841. /**
  842. * Tell the statemanager there is a request to delete the selected messages
  843. * and run the renderer to show confirm delete messages dialogue.
  844. *
  845. * @param {Number} userId User id.
  846. */
  847. var requestDeleteSelectedMessages = function(userId) {
  848. var selectedMessageIds = viewState.selectedMessageIds;
  849. cancelRequest(userId);
  850. var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds);
  851. render(newState);
  852. };
  853. /**
  854. * Send the repository a request to delete the messages pending deletion. Update the statemanager
  855. * and publish a message deletion event.
  856. *
  857. * @return {Promise} Renderer promise.
  858. */
  859. var deleteSelectedMessages = function() {
  860. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:deleteSelectedMessages');
  861. var messageIds = viewState.pendingDeleteMessageIds;
  862. var sentMessages = viewState.messages.filter(function(message) {
  863. // If a message sendState is null then it means it was loaded from the server or if it's
  864. // set to sent then it means the user has successfully sent it in this page load.
  865. return messageIds.indexOf(message.id) >= 0 && (message.sendState == 'sent' || message.sendState === null);
  866. });
  867. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  868. render(newState);
  869. var deleteMessagesPromise = $.Deferred().resolve().promise();
  870. if (sentMessages.length) {
  871. // We only need to send a request to the server if we're trying to delete messages that
  872. // have successfully been sent.
  873. var sentMessageIds = sentMessages.map(function(message) {
  874. return message.id;
  875. });
  876. if (newState.deleteMessagesForAllUsers) {
  877. deleteMessagesPromise = Repository.deleteMessagesForAllUsers(viewState.loggedInUserId, sentMessageIds);
  878. } else {
  879. deleteMessagesPromise = Repository.deleteMessages(viewState.loggedInUserId, sentMessageIds);
  880. }
  881. }
  882. // Mark that we are deleting content from the conversation to prevent updates of it.
  883. isDeletingConversationContent = true;
  884. // Stop polling for new messages to the open conversation.
  885. if (newMessagesPollTimer) {
  886. newMessagesPollTimer.stop();
  887. }
  888. return deleteMessagesPromise.then(function() {
  889. var newState = StateManager.removeMessagesById(viewState, messageIds);
  890. newState = StateManager.removePendingDeleteMessagesById(newState, messageIds);
  891. newState = StateManager.removeSelectedMessagesById(newState, messageIds);
  892. newState = StateManager.setLoadingConfirmAction(newState, false);
  893. newState = StateManager.setDeleteMessagesForAllUsers(newState, false);
  894. var prevLastMessage = viewState.messages[viewState.messages.length - 1];
  895. var newLastMessage = newState.messages.length ? newState.messages[newState.messages.length - 1] : null;
  896. if (newLastMessage && newLastMessage.id != prevLastMessage.id) {
  897. var conversation = formatConversationForEvent(newState);
  898. PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
  899. } else if (!newState.messages.length) {
  900. PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
  901. }
  902. isDeletingConversationContent = false;
  903. return render(newState);
  904. })
  905. .then(function(result) {
  906. pendingPromise.resolve();
  907. return result;
  908. })
  909. .catch(Notification.exception);
  910. };
  911. /**
  912. * Tell the statemanager there is a request to delete a conversation
  913. * and run the renderer to show confirm delete conversation dialogue.
  914. *
  915. * @param {Number} userId User id of other user.
  916. */
  917. var requestDeleteConversation = function(userId) {
  918. cancelRequest(userId);
  919. var newState = StateManager.setPendingDeleteConversation(viewState, true);
  920. render(newState);
  921. };
  922. /**
  923. * Send the repository a request to delete a conversation. Update the statemanager
  924. * and publish a conversation deleted event.
  925. *
  926. * @return {Promise} Renderer promise.
  927. */
  928. var deleteConversation = function() {
  929. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');
  930. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  931. render(newState);
  932. // Mark that we are deleting the conversation to prevent updates of it.
  933. isDeletingConversationContent = true;
  934. // Stop polling for new messages to the open conversation.
  935. if (newMessagesPollTimer) {
  936. newMessagesPollTimer.stop();
  937. }
  938. return Repository.deleteConversation(viewState.loggedInUserId, viewState.id)
  939. .then(function() {
  940. var newState = StateManager.removeMessages(viewState, viewState.messages);
  941. newState = StateManager.removeSelectedMessagesById(newState, viewState.selectedMessageIds);
  942. newState = StateManager.setPendingDeleteConversation(newState, false);
  943. newState = StateManager.setLoadingConfirmAction(newState, false);
  944. PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
  945. isDeletingConversationContent = false;
  946. return render(newState);
  947. })
  948. .then(function(result) {
  949. pendingPromise.resolve();
  950. return result;
  951. });
  952. };
  953. /**
  954. * Tell the statemanager to cancel all pending actions.
  955. *
  956. * @param {Number} userId User id.
  957. */
  958. var cancelRequest = function(userId) {
  959. var pendingDeleteMessageIds = viewState.pendingDeleteMessageIds;
  960. var newState = StateManager.removePendingAddContactsById(viewState, [userId]);
  961. newState = StateManager.removePendingRemoveContactsById(newState, [userId]);
  962. newState = StateManager.removePendingUnblockUsersById(newState, [userId]);
  963. newState = StateManager.removePendingBlockUsersById(newState, [userId]);
  964. newState = StateManager.removePendingDeleteMessagesById(newState, pendingDeleteMessageIds);
  965. newState = StateManager.setPendingDeleteConversation(newState, false);
  966. newState = StateManager.setDeleteMessagesForAllUsers(newState, false);
  967. render(newState);
  968. };
  969. /**
  970. * Accept the contact request from the given user.
  971. *
  972. * @param {Number} userId User id of other user.
  973. * @return {Promise} Renderer promise.
  974. */
  975. var acceptContactRequest = function(userId) {
  976. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:acceptContactRequest');
  977. // Search the list of the logged in user's contact requests to find the
  978. // one from this user.
  979. var loggedInUserId = viewState.loggedInUserId;
  980. var requests = viewState.members[userId].contactrequests.filter(function(request) {
  981. return request.requesteduserid == loggedInUserId;
  982. });
  983. var request = requests[0];
  984. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  985. render(newState);
  986. return Repository.acceptContactRequest(userId, loggedInUserId)
  987. .then(function(profile) {
  988. var newState = StateManager.removeContactRequests(viewState, [request]);
  989. newState = StateManager.addMembers(viewState, [profile]);
  990. newState = StateManager.setLoadingConfirmAction(newState, false);
  991. return render(newState);
  992. })
  993. .then(function() {
  994. PubSub.publish(MessageDrawerEvents.CONTACT_ADDED, viewState.members[userId]);
  995. PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, request);
  996. return;
  997. })
  998. .then(function(result) {
  999. pendingPromise.resolve();
  1000. return result;
  1001. });
  1002. };
  1003. /**
  1004. * Decline the contact request from the given user.
  1005. *
  1006. * @param {Number} userId User id of other user.
  1007. * @return {Promise} Renderer promise.
  1008. */
  1009. var declineContactRequest = function(userId) {
  1010. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:declineContactRequest');
  1011. // Search the list of the logged in user's contact requests to find the
  1012. // one from this user.
  1013. var loggedInUserId = viewState.loggedInUserId;
  1014. var requests = viewState.members[userId].contactrequests.filter(function(request) {
  1015. return request.requesteduserid == loggedInUserId;
  1016. });
  1017. var request = requests[0];
  1018. var newState = StateManager.setLoadingConfirmAction(viewState, true);
  1019. render(newState);
  1020. return Repository.declineContactRequest(userId, loggedInUserId)
  1021. .then(function(profile) {
  1022. var newState = StateManager.removeContactRequests(viewState, [request]);
  1023. newState = StateManager.addMembers(viewState, [profile]);
  1024. newState = StateManager.setLoadingConfirmAction(newState, false);
  1025. return render(newState);
  1026. })
  1027. .then(function() {
  1028. PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, request);
  1029. return;
  1030. })
  1031. .then(function(result) {
  1032. pendingPromise.resolve();
  1033. return result;
  1034. });
  1035. };
  1036. /**
  1037. * Send all of the messages in the buffer to the server to be created. Update the
  1038. * UI with the newly created message information.
  1039. *
  1040. * This function will recursively call itself in order to make sure the buffer is
  1041. * always being processed.
  1042. */
  1043. var processSendMessageBuffer = function() {
  1044. if (isSendingMessage) {
  1045. // We're already sending messages so nothing to do.
  1046. return;
  1047. }
  1048. if (!sendMessageBuffer.length) {
  1049. // No messages waiting to send. Nothing to do.
  1050. return;
  1051. }
  1052. var pendingPromise = new Pending('core_message/message_drawer_view_conversation:processSendMessageBuffer');
  1053. // Flag that we're processing the queue.
  1054. isSendingMessage = true;
  1055. // Grab all of the messages in the buffer.
  1056. var messagesToSend = sendMessageBuffer.slice();
  1057. // Empty the buffer since we're processing it.
  1058. sendMessageBuffer = [];
  1059. var conversationId = viewState.id;
  1060. var newConversationId = null;
  1061. var messagesText = messagesToSend.map(function(message) {
  1062. return message.text;
  1063. });
  1064. var messageIds = messagesToSend.map(function(message) {
  1065. return message.id;
  1066. });
  1067. var sendMessagePromise = null;
  1068. var newCanDeleteMessagesForAllUsers = null;
  1069. if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {
  1070. // If it's a new private conversation then we need to use the old
  1071. // web service function to create the conversation.
  1072. var otherUserId = getOtherUserId();
  1073. sendMessagePromise = Repository.sendMessagesToUser(otherUserId, messagesText)
  1074. .then(function(messages) {
  1075. if (messages.length) {
  1076. newConversationId = parseInt(messages[0].conversationid, 10);
  1077. newCanDeleteMessagesForAllUsers = messages[0].candeletemessagesforallusers;
  1078. }
  1079. return messages;
  1080. });
  1081. } else {
  1082. sendMessagePromise = Repository.sendMessagesToConversation(conversationId, messagesText);
  1083. }
  1084. sendMessagePromise
  1085. .then(function(messages) {
  1086. var newMessageIds = messages.map(function(message) {
  1087. return message.id;
  1088. });
  1089. var data = [];
  1090. var selectedToRemove = [];
  1091. var selectedToAdd = [];
  1092. messagesToSend.forEach(function(oldMessage, index) {
  1093. var newMessage = messages[index];
  1094. // Update messages expects and array of arrays where the first value
  1095. // is the old message to update and the second value is the new values
  1096. // to set.
  1097. data.push([oldMessage, newMessage]);
  1098. if (viewState.selectedMessageIds.indexOf(oldMessage.id) >= 0) {
  1099. // If the message was added to the "selected messages" list while it was still
  1100. // being sent then we should update it's id in that list now to make sure future
  1101. // actions work.
  1102. selectedToRemove.push(oldMessage.id);
  1103. selectedToAdd.push(newMessage.id);
  1104. }
  1105. });
  1106. var newState = StateManager.updateMessages(viewState, data);
  1107. newState = StateManager.setMessagesSendSuccessById(newState, newMessageIds);
  1108. if (selectedToRemove.length) {
  1109. newState = StateManager.removeSelectedMessagesById(newState, selectedToRemove);
  1110. }
  1111. if (selectedToAdd.length) {
  1112. newState = StateManager.addSelectedMessagesById(newState, selectedToAdd);
  1113. }
  1114. var conversation = formatConversationForEvent(newState);
  1115. if (!newState.id) {
  1116. // If this message created the conversation then save the conversation
  1117. // id.
  1118. newState = StateManager.setId(newState, newConversationId);
  1119. conversation.id = newConversationId;
  1120. resetMessagePollTimer(newConversationId);
  1121. PubSub.publish(MessageDrawerEvents.CONVERSATION_CREATED, conversation);
  1122. newState = StateManager.setCanDeleteMessagesForAllUsers(newState, newCanDeleteMessagesForAllUsers);
  1123. }
  1124. // Update the UI with the new message values from the server.
  1125. render(newState);
  1126. // Recurse just in case there has been more messages added to the buffer.
  1127. isSendingMessage = false;
  1128. processSendMessageBuffer();
  1129. PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
  1130. return;
  1131. })
  1132. .then(function(result) {
  1133. pendingPromise.resolve();
  1134. return result;
  1135. })
  1136. .catch(function(e) {
  1137. var errorMessage;
  1138. if (e.message) {
  1139. errorMessage = $.Deferred().resolve(e.message).promise();
  1140. } else {
  1141. errorMessage = Str.get_string('unknownerror', 'core');
  1142. }
  1143. var handleFailedMessages = function(errorMessage) {
  1144. // We failed to create messages so remove the old messages from the pending queue
  1145. // and update the UI to indicate that the message failed.
  1146. var newState = StateManager.setMessagesSendFailById(viewState, messageIds, errorMessage);
  1147. render(newState);
  1148. isSendingMessage = false;
  1149. processSendMessageBuffer();
  1150. };
  1151. errorMessage.then(handleFailedMessages)
  1152. .then(function(result) {
  1153. pendingPromise.resolve();
  1154. return result;
  1155. })
  1156. .catch(function(e) {
  1157. // Hrmm, we can't even load the error messages string! We'll have to
  1158. // hard code something in English here if we still haven't got a message
  1159. // to show.
  1160. var finalError = e.message || 'Something went wrong!';
  1161. handleFailedMessages(finalError);
  1162. });
  1163. });
  1164. };
  1165. /**
  1166. * Create a plain version of an HTML text.
  1167. *
  1168. * This texts is used as a message preview while is sent to the server. This way
  1169. * it is possible to prevent self-xss.
  1170. *
  1171. * @param {String} text Text to send.
  1172. * @return {String} The plain text version of the text.
  1173. */
  1174. const previewText = function(text) {
  1175. // Remove all script and styles from text (we don't want it there).
  1176. let plaintext = text.replace(/<style([\s\S]*?)<\/style>/gi, '');
  1177. plaintext = plaintext.replace(/<script([\s\S]*?)<\/script>/gi, '');
  1178. // Beautify a bit the output adding some line breaks.
  1179. plaintext = plaintext.replace(/<\/div>/ig, '\n');
  1180. plaintext = plaintext.replace(/<\/li>/ig, '\n');
  1181. plaintext = plaintext.replace(/<li>/ig, ' * ');
  1182. plaintext = plaintext.replace(/<\/ul>/ig, '\n');
  1183. plaintext = plaintext.replace(/<\/p>/ig, '\n');
  1184. plaintext = plaintext.replace(/<br[^>]*>/gi, '\n');
  1185. // Remove all remaining tags and convert line breaks into html.
  1186. plaintext = plaintext.replace(/<[^>]+>/ig, '');
  1187. plaintext = plaintext.replace(/\n+/ig, '\n');
  1188. return plaintext.replace(/\n/ig, '<br>');
  1189. };
  1190. /**
  1191. * Buffers messages to be sent to the server. We use a buffer here to allow the
  1192. * user to freely input messages without blocking the interface for them.
  1193. *
  1194. * Instead we just queue all of their messages up and send them as fast as we can.
  1195. *
  1196. * @param {String} text Text to send.
  1197. */
  1198. var sendMessage = function(text) {
  1199. var id = 'temp' + Date.now();
  1200. // Render a preview version of the message while sending.
  1201. let loadingmessage = {
  1202. id: id,
  1203. useridfrom: viewState.loggedInUserId,
  1204. text: previewText(text),
  1205. timecreated: null
  1206. };
  1207. var newState = StateManager.addMessages(viewState, [loadingmessage]);
  1208. render(newState);
  1209. // Send the real message.
  1210. var message = {
  1211. id: id,
  1212. useridfrom: viewState.loggedInUserId,
  1213. text: text,
  1214. timecreated: null
  1215. };
  1216. sendMessageBuffer.push(message);
  1217. processSendMessageBuffer();
  1218. };
  1219. /**
  1220. * Retry sending a message that failed.
  1221. *
  1222. * @param {Object} message The message to send.
  1223. */
  1224. var retrySendMessage = function(message) {
  1225. var newState = StateManager.setMessagesSendPendingById(viewState, [message.id]);
  1226. render(newState);
  1227. sendMessageBuffer.push(message);
  1228. processSendMessageBuffer();
  1229. };
  1230. /**
  1231. * Toggle the selected messages update the statemanager and render the result.
  1232. *
  1233. * @param {Number} messageId The id of the message to be toggled
  1234. */
  1235. var toggleSelectMessage = function(messageId) {
  1236. var newState = viewState;
  1237. if (viewState.selectedMessageIds.indexOf(messageId) > -1) {
  1238. newState = StateManager.removeSelectedMessagesById(viewState, [messageId]);
  1239. } else {
  1240. newState = StateManager.addSelectedMessagesById(viewState, [messageId]);
  1241. }
  1242. render(newState);
  1243. };
  1244. /**
  1245. * Cancel edit mode (selecting the messages).
  1246. */
  1247. var cancelEditMode = function() {
  1248. cancelRequest(getOtherUserId());
  1249. var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds);
  1250. render(newState);
  1251. };
  1252. /**
  1253. * Process the patches in the render buffer one at a time in order until the
  1254. * buffer is empty.
  1255. *
  1256. * @param {Object} header The conversation header container element.
  1257. * @param {Object} body The conversation body container element.
  1258. * @param {Object} footer The conversation footer container element.
  1259. */
  1260. var processRenderBuffer = function(header, body, footer) {
  1261. if (isRendering) {
  1262. return;
  1263. }
  1264. if (!renderBuffer.length) {
  1265. return;
  1266. }
  1267. isRendering = true;
  1268. var renderable = renderBuffer.shift();
  1269. var renderPromises = renderers.map(function(renderFunc) {
  1270. return renderFunc(renderable.patch);
  1271. });
  1272. $.when.apply(null, renderPromises)
  1273. .then(function() {
  1274. isRendering = false;
  1275. renderable.deferred.resolve(true);
  1276. // Keep processing the buffer until it's empty.
  1277. processRenderBuffer(header, body, footer);
  1278. return;
  1279. })
  1280. .catch(function(error) {
  1281. isRendering = false;
  1282. renderable.deferred.reject(error);
  1283. Notification.exception(error);
  1284. });
  1285. };
  1286. /**
  1287. * Create a function to render the Conversation.
  1288. *
  1289. * @param {Object} header The conversation header container element.
  1290. * @param {Object} body The conversation body container element.
  1291. * @param {Object} footer The conversation footer container element.
  1292. * @param {Bool} isNewConversation Has someone else already initialised a conversation?
  1293. * @return {Promise} Renderer promise.
  1294. */
  1295. var generateRenderFunction = function(header, body, footer, isNewConversation) {
  1296. var rendererFunc = function(patch) {
  1297. return Renderer.render(header, body, footer, patch);
  1298. };
  1299. if (!isNewConversation) {
  1300. // Looks like someone got here before us! We'd better update our
  1301. // UI to make sure it matches.
  1302. var initialState = StateManager.buildInitialState(viewState.midnight, viewState.loggedInUserId, viewState.id);
  1303. var syncPatch = Patcher.buildPatch(initialState, viewState);
  1304. rendererFunc(syncPatch);
  1305. }
  1306. renderers.push(rendererFunc);
  1307. return function(newState) {
  1308. var patch = Patcher.buildPatch(viewState, newState);
  1309. var deferred = $.Deferred();
  1310. // Check if the patch has any data. Ignore empty patches.
  1311. if (Object.keys(patch).length) {
  1312. // Add the patch to the render buffer which gets processed in order.
  1313. renderBuffer.push({
  1314. patch: patch,
  1315. deferred: deferred
  1316. });
  1317. } else {
  1318. deferred.resolve(true);
  1319. }
  1320. // This is a great place to add in some console logging if you need
  1321. // to debug something. You can log the current state, the next state,
  1322. // and the generated patch and see exactly what will be updated.
  1323. // Optimistically update the state. We're going to assume that the rendering
  1324. // will always succeed. The rendering is asynchronous (annoyingly) so it's buffered
  1325. // but it'll reach eventual consistency with the current state.
  1326. viewState = newState;
  1327. if (newState.id) {
  1328. // Only cache created conversations.
  1329. stateCache[newState.id] = {
  1330. state: newState,
  1331. messagesOffset: getMessagesOffset(),
  1332. loadedAllMessages: hasLoadedAllMessages()
  1333. };
  1334. }
  1335. // Start processing the buffer.
  1336. processRenderBuffer(header, body, footer);
  1337. return deferred.promise();
  1338. };
  1339. };
  1340. /**
  1341. * Create a confirm action function.
  1342. *
  1343. * @param {Function} actionCallback The callback function.
  1344. * @return {Function} Confirm action handler.
  1345. */
  1346. var generateConfirmActionHandler = function(actionCallback) {
  1347. return function(e, data) {
  1348. if (!viewState.loadingConfirmAction) {
  1349. actionCallback(getOtherUserId());
  1350. var newState = StateManager.setLoadingConfirmAction(viewState, false);
  1351. render(newState);
  1352. }
  1353. data.originalEvent.preventDefault();
  1354. };
  1355. };
  1356. /**
  1357. * Send message event handler.
  1358. *
  1359. * @param {Object} e Element this event handler is called on.
  1360. * @param {Object} data Data for this event.
  1361. */
  1362. var handleSendMessage = function(e, data) {
  1363. var target = $(e.target);
  1364. var footerContainer = target.closest(SELECTORS.FOOTER_CONTAINER);
  1365. var textArea = footerContainer.find(SELECTORS.MESSAGE_TEXT_AREA);
  1366. var text = textArea.val().trim();
  1367. if (text !== '') {
  1368. sendMessage(text);
  1369. textArea.val('');
  1370. textArea.focus();
  1371. }
  1372. data.originalEvent.preventDefault();
  1373. };
  1374. /**
  1375. * Select message event handler.
  1376. *
  1377. * @param {Object} e Element this event handler is called on.
  1378. * @param {Object} data Data for this event.
  1379. */
  1380. var handleSelectMessage = function(e, data) {
  1381. var selection = window.getSelection();
  1382. var target = $(e.target);
  1383. if (selection.toString() != '') {
  1384. // Bail if we're selecting.
  1385. return;
  1386. }
  1387. if (target.is('a')) {
  1388. // Clicking on a link in the message so ignore it.
  1389. return;
  1390. }
  1391. var element = target.closest(SELECTORS.MESSAGE);
  1392. var messageId = element.attr('data-message-id');
  1393. toggleSelectMessage(messageId);
  1394. data.originalEvent.preventDefault();
  1395. };
  1396. /**
  1397. * Handle retry sending of message.
  1398. *
  1399. * @param {Object} e Element this event handler is called on.
  1400. * @param {Object} data Data for this event.
  1401. */
  1402. var handleRetrySendMessage = function(e, data) {
  1403. var target = $(e.target);
  1404. var element = target.closest(SELECTORS.MESSAGE);
  1405. var messageId = element.attr('data-message-id');
  1406. var messages = viewState.messages.filter(function(message) {
  1407. return message.id == messageId;
  1408. });
  1409. var message = messages.length ? messages[0] : null;
  1410. if (message) {
  1411. retrySendMessage(message);
  1412. }
  1413. data.originalEvent.preventDefault();
  1414. data.originalEvent.stopPropagation();
  1415. e.stopPropagation();
  1416. };
  1417. /**
  1418. * Cancel edit mode event handler.
  1419. *
  1420. * @param {Object} e Element this event handler is called on.
  1421. * @param {Object} data Data for this event.
  1422. */
  1423. var handleCancelEditMode = function(e, data) {
  1424. cancelEditMode();
  1425. data.originalEvent.preventDefault();
  1426. };
  1427. /**
  1428. * Show the view contact page.
  1429. *
  1430. * @param {String} namespace Unique identifier for the Routes
  1431. * @return {Function} View contact handler.
  1432. */
  1433. var generateHandleViewContact = function(namespace) {
  1434. return function(e, data) {
  1435. var otherUserId = getOtherUserId();
  1436. var otherUser = viewState.members[otherUserId];
  1437. MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONTACT, otherUser);
  1438. data.originalEvent.preventDefault();
  1439. };
  1440. };
  1441. /**
  1442. * Set this conversation as a favourite.
  1443. *
  1444. * @param {Object} e Element this event handler is called on.
  1445. * @param {Object} data Data for this event.
  1446. */
  1447. var handleSetFavourite = function(e, data) {
  1448. setFavourite().catch(Notification.exception);
  1449. data.originalEvent.preventDefault();
  1450. };
  1451. /**
  1452. * Unset this conversation as a favourite.
  1453. *
  1454. * @param {Object} e Element this event handler is called on.
  1455. * @param {Object} data Data for this event.
  1456. */
  1457. var handleUnsetFavourite = function(e, data) {
  1458. unsetFavourite().catch(Notification.exception);
  1459. data.originalEvent.preventDefault();
  1460. };
  1461. /**
  1462. * Show the view group info page.
  1463. * Set this conversation as muted.
  1464. *
  1465. * @param {Object} e Element this event handler is called on.
  1466. * @param {Object} data Data for this event.
  1467. */
  1468. var handleSetMuted = function(e, data) {
  1469. setMuted().catch(Notification.exception);
  1470. data.originalEvent.preventDefault();
  1471. };
  1472. /**
  1473. * Unset this conversation as muted.
  1474. *
  1475. * @param {Object} e Element this event handler is called on.
  1476. * @param {Object} data Data for this event.
  1477. */
  1478. var handleUnsetMuted = function(e, data) {
  1479. unsetMuted().catch(Notification.exception);
  1480. data.originalEvent.preventDefault();
  1481. };
  1482. /**
  1483. * Handle clicking on the checkbox that toggles deleting messages for
  1484. * all users.
  1485. *
  1486. * @param {Object} e Element this event handler is called on.
  1487. */
  1488. var handleDeleteMessagesForAllUsersToggle = function(e) {
  1489. var newValue = $(e.target).prop('checked');
  1490. var newState = StateManager.setDeleteMessagesForAllUsers(viewState, newValue);
  1491. render(newState);
  1492. };
  1493. /**
  1494. * Show the view contact page.
  1495. *
  1496. * @param {String} namespace Unique identifier for the Routes
  1497. * @return {Function} View group info handler.
  1498. */
  1499. var generateHandleViewGroupInfo = function(namespace) {
  1500. return function(e, data) {
  1501. MessageDrawerRouter.go(
  1502. namespace,
  1503. MessageDrawerRoutes.VIEW_GROUP_INFO,
  1504. {
  1505. id: viewState.id,
  1506. name: viewState.name,
  1507. subname: viewState.subname,
  1508. imageUrl: viewState.imageUrl,
  1509. totalMemberCount: viewState.totalMemberCount
  1510. },
  1511. viewState.loggedInUserId
  1512. );
  1513. data.originalEvent.preventDefault();
  1514. };
  1515. };
  1516. /**
  1517. * Handle clicking on the emoji toggle button.
  1518. *
  1519. * @param {Object} e The event
  1520. * @param {Object} data The custom interaction event data
  1521. */
  1522. var handleToggleEmojiPicker = function(e, data) {
  1523. var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);
  1524. render(newState);
  1525. data.originalEvent.preventDefault();
  1526. };
  1527. /**
  1528. * Handle clicking outside the emoji picker to close it.
  1529. *
  1530. * @param {Object} e The event
  1531. */
  1532. var handleCloseEmojiPicker = function(e) {
  1533. var target = $(e.target);
  1534. if (
  1535. viewState.showEmojiPicker &&
  1536. !target.closest(SELECTORS.EMOJI_PICKER_CONTAINER).length &&
  1537. !target.closest(SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON).length
  1538. ) {
  1539. var newState = StateManager.setShowEmojiPicker(viewState, false);
  1540. render(newState);
  1541. }
  1542. };
  1543. /**
  1544. * Listen to, and handle events for conversations.
  1545. *
  1546. * @param {string} namespace The route namespace.
  1547. * @param {Object} header Conversation header container element.
  1548. * @param {Object} body Conversation body container element.
  1549. * @param {Object} footer Conversation footer container element.
  1550. */
  1551. var registerEventListeners = function(namespace, header, body, footer) {
  1552. var isLoadingMoreMessages = false;
  1553. var messagesContainer = getMessagesContainer(body);
  1554. var emojiPickerElement = footer.find(SELECTORS.EMOJI_PICKER);
  1555. var emojiAutoCompleteContainer = footer.find(SELECTORS.EMOJI_AUTO_COMPLETE_CONTAINER);
  1556. var messageTextArea = footer.find(SELECTORS.MESSAGE_TEXT_AREA);
  1557. var headerActivateHandlers = [
  1558. [SELECTORS.ACTION_REQUEST_BLOCK, generateConfirmActionHandler(requestBlockUser)],
  1559. [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],
  1560. [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
  1561. [SELECTORS.ACTION_REQUEST_REMOVE_CONTACT, generateConfirmActionHandler(requestRemoveContact)],
  1562. [SELECTORS.ACTION_REQUEST_DELETE_CONVERSATION, generateConfirmActionHandler(requestDeleteConversation)],
  1563. [SELECTORS.ACTION_CANCEL_EDIT_MODE, handleCancelEditMode],
  1564. [SELECTORS.ACTION_VIEW_CONTACT, generateHandleViewContact(namespace)],
  1565. [SELECTORS.ACTION_VIEW_GROUP_INFO, generateHandleViewGroupInfo(namespace)],
  1566. [SELECTORS.ACTION_CONFIRM_FAVOURITE, handleSetFavourite],
  1567. [SELECTORS.ACTION_CONFIRM_MUTE, handleSetMuted],
  1568. [SELECTORS.ACTION_CONFIRM_UNFAVOURITE, handleUnsetFavourite],
  1569. [SELECTORS.ACTION_CONFIRM_UNMUTE, handleUnsetMuted]
  1570. ];
  1571. var bodyActivateHandlers = [
  1572. [SELECTORS.ACTION_CANCEL_CONFIRM, generateConfirmActionHandler(cancelRequest)],
  1573. [SELECTORS.ACTION_CONFIRM_BLOCK, generateConfirmActionHandler(blockUser)],
  1574. [SELECTORS.ACTION_CONFIRM_UNBLOCK, generateConfirmActionHandler(unblockUser)],
  1575. [SELECTORS.ACTION_CONFIRM_ADD_CONTACT, generateConfirmActionHandler(addContact)],
  1576. [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT, generateConfirmActionHandler(removeContact)],
  1577. [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(deleteSelectedMessages)],
  1578. [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION, generateConfirmActionHandler(deleteConversation)],
  1579. [SELECTORS.ACTION_OKAY_CONFIRM, generateConfirmActionHandler(cancelRequest)],
  1580. [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
  1581. [SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST, generateConfirmActionHandler(acceptContactRequest)],
  1582. [SELECTORS.ACTION_DECLINE_CONTACT_REQUEST, generateConfirmActionHandler(declineContactRequest)],
  1583. [SELECTORS.MESSAGE, handleSelectMessage],
  1584. [SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE, handleDeleteMessagesForAllUsersToggle],
  1585. [SELECTORS.RETRY_SEND, handleRetrySendMessage]
  1586. ];
  1587. var footerActivateHandlers = [
  1588. [SELECTORS.SEND_MESSAGE_BUTTON, handleSendMessage],
  1589. [SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON, handleToggleEmojiPicker],
  1590. [SELECTORS.ACTION_REQUEST_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(requestDeleteSelectedMessages)],
  1591. [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
  1592. [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],
  1593. ];
  1594. AutoRows.init(footer);
  1595. if (emojiAutoCompleteContainer.length) {
  1596. initialiseEmojiAutoComplete(
  1597. emojiAutoCompleteContainer[0],
  1598. messageTextArea[0],
  1599. function(hasSuggestions) {
  1600. var newState = StateManager.setShowEmojiAutoComplete(viewState, hasSuggestions);
  1601. render(newState);
  1602. },
  1603. function(emoji) {
  1604. var newState = StateManager.setShowEmojiAutoComplete(viewState, false);
  1605. render(newState);
  1606. messageTextArea.focus();
  1607. var cursorPos = messageTextArea.prop('selectionStart');
  1608. var currentText = messageTextArea.val();
  1609. var textBefore = currentText.substring(0, cursorPos).replace(/\S*$/, '');
  1610. var textAfter = currentText.substring(cursorPos).replace(/^\S*/, '');
  1611. messageTextArea.val(textBefore + emoji + textAfter);
  1612. // Set the cursor position to after the inserted emoji.
  1613. messageTextArea.prop('selectionStart', textBefore.length + emoji.length);
  1614. messageTextArea.prop('selectionEnd', textBefore.length + emoji.length);
  1615. }
  1616. );
  1617. }
  1618. if (emojiPickerElement.length) {
  1619. initialiseEmojiPicker(emojiPickerElement[0], function(emoji) {
  1620. var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);
  1621. render(newState);
  1622. messageTextArea.focus();
  1623. var cursorPos = messageTextArea.prop('selectionStart');
  1624. var currentText = messageTextArea.val();
  1625. var textBefore = currentText.substring(0, cursorPos);
  1626. var textAfter = currentText.substring(cursorPos, currentText.length);
  1627. messageTextArea.val(textBefore + emoji + textAfter);
  1628. // Set the cursor position to after the inserted emoji.
  1629. messageTextArea.prop('selectionStart', cursorPos + emoji.length);
  1630. messageTextArea.prop('selectionEnd', cursorPos + emoji.length);
  1631. });
  1632. }
  1633. CustomEvents.define(header, [
  1634. CustomEvents.events.activate
  1635. ]);
  1636. CustomEvents.define(body, [
  1637. CustomEvents.events.activate
  1638. ]);
  1639. CustomEvents.define(footer, [
  1640. CustomEvents.events.activate,
  1641. CustomEvents.events.enter,
  1642. CustomEvents.events.escape
  1643. ]);
  1644. CustomEvents.define(messagesContainer, [
  1645. CustomEvents.events.scrollTop,
  1646. CustomEvents.events.scrollLock
  1647. ]);
  1648. messagesContainer.on(CustomEvents.events.scrollTop, function(e, data) {
  1649. var hasMembers = Object.keys(viewState.members).length > 1;
  1650. if (!isResetting && !isLoadingMoreMessages && !hasLoadedAllMessages() && hasMembers) {
  1651. isLoadingMoreMessages = true;
  1652. var newState = StateManager.setLoadingMessages(viewState, true);
  1653. render(newState);
  1654. loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, [])
  1655. .then(function() {
  1656. isLoadingMoreMessages = false;
  1657. setMessagesOffset(getMessagesOffset() + LOAD_MESSAGE_LIMIT);
  1658. return;
  1659. })
  1660. .catch(function(error) {
  1661. isLoadingMoreMessages = false;
  1662. Notification.exception(error);
  1663. });
  1664. }
  1665. data.originalEvent.preventDefault();
  1666. });
  1667. headerActivateHandlers.forEach(function(handler) {
  1668. var selector = handler[0];
  1669. var handlerFunction = handler[1];
  1670. header.on(CustomEvents.events.activate, selector, handlerFunction);
  1671. });
  1672. bodyActivateHandlers.forEach(function(handler) {
  1673. var selector = handler[0];
  1674. var handlerFunction = handler[1];
  1675. body.on(CustomEvents.events.activate, selector, handlerFunction);
  1676. });
  1677. footerActivateHandlers.forEach(function(handler) {
  1678. var selector = handler[0];
  1679. var handlerFunction = handler[1];
  1680. footer.on(CustomEvents.events.activate, selector, handlerFunction);
  1681. });
  1682. footer.on(CustomEvents.events.enter, SELECTORS.MESSAGE_TEXT_AREA, function(e, data) {
  1683. var enterToSend = footer.attr('data-enter-to-send');
  1684. if (enterToSend && enterToSend != 'false' && enterToSend != '0') {
  1685. handleSendMessage(e, data);
  1686. }
  1687. });
  1688. footer.on(CustomEvents.events.escape, SELECTORS.EMOJI_PICKER_CONTAINER, handleToggleEmojiPicker);
  1689. $(document.body).on('click', handleCloseEmojiPicker);
  1690. PubSub.subscribe(MessageDrawerEvents.ROUTE_CHANGED, function(newRouteData) {
  1691. if (newMessagesPollTimer) {
  1692. if (newRouteData.route != MessageDrawerRoutes.VIEW_CONVERSATION) {
  1693. newMessagesPollTimer.stop();
  1694. }
  1695. }
  1696. });
  1697. };
  1698. /**
  1699. * Reset the timer that polls for new messages.
  1700. *
  1701. * @param {Number} conversationId The conversation id
  1702. */
  1703. var resetMessagePollTimer = function(conversationId) {
  1704. if (newMessagesPollTimer) {
  1705. newMessagesPollTimer.stop();
  1706. }
  1707. newMessagesPollTimer = new BackOffTimer(
  1708. getLoadNewMessagesCallback(conversationId, NEWEST_FIRST),
  1709. BackOffTimer.getIncrementalCallback(
  1710. viewState.messagePollMin * MILLISECONDS_IN_SEC,
  1711. MILLISECONDS_IN_SEC,
  1712. viewState.messagePollMax * MILLISECONDS_IN_SEC,
  1713. viewState.messagePollAfterMax * MILLISECONDS_IN_SEC
  1714. )
  1715. );
  1716. newMessagesPollTimer.start();
  1717. };
  1718. /**
  1719. * Reset the state to the initial state and render the UI.
  1720. *
  1721. * @param {Object} body Conversation body container element.
  1722. * @param {Number|null} conversationId The conversation id.
  1723. * @param {Object} loggedInUserProfile The logged in user's profile.
  1724. */
  1725. var resetState = function(body, conversationId, loggedInUserProfile) {
  1726. // Reset all of the states back to the beginning if we're loading a new
  1727. // conversation.
  1728. if (newMessagesPollTimer) {
  1729. newMessagesPollTimer.stop();
  1730. }
  1731. loadedAllMessages = false;
  1732. messagesOffset = 0;
  1733. newMessagesPollTimer = null;
  1734. isRendering = false;
  1735. renderBuffer = [];
  1736. isResetting = true;
  1737. isSendingMessage = false;
  1738. isDeletingConversationContent = false;
  1739. sendMessageBuffer = [];
  1740. var loggedInUserId = loggedInUserProfile.id;
  1741. var midnight = parseInt(body.attr('data-midnight'), 10);
  1742. var messagePollMin = parseInt(body.attr('data-message-poll-min'), 10);
  1743. var messagePollMax = parseInt(body.attr('data-message-poll-max'), 10);
  1744. var messagePollAfterMax = parseInt(body.attr('data-message-poll-after-max'), 10);
  1745. var initialState = StateManager.buildInitialState(
  1746. midnight,
  1747. loggedInUserId,
  1748. conversationId,
  1749. messagePollMin,
  1750. messagePollMax,
  1751. messagePollAfterMax
  1752. );
  1753. if (!viewState) {
  1754. viewState = initialState;
  1755. }
  1756. render(initialState);
  1757. };
  1758. /**
  1759. * Load a new empty private conversation between two users or self-conversation.
  1760. *
  1761. * @param {Object} body Conversation body container element.
  1762. * @param {Object} loggedInUserProfile The logged in user's profile.
  1763. * @param {Int} otherUserId The other user's id.
  1764. * @return {Promise} Renderer promise.
  1765. */
  1766. var resetNoConversation = function(body, loggedInUserProfile, otherUserId) {
  1767. // Always reset the state back to the initial state so that the
  1768. // state manager and patcher can work correctly.
  1769. resetState(body, null, loggedInUserProfile);
  1770. var resetNoConversationPromise = null;
  1771. if (loggedInUserProfile.id != otherUserId) {
  1772. // Private conversation between two different users.
  1773. resetNoConversationPromise = Repository.getConversationBetweenUsers(
  1774. loggedInUserProfile.id,
  1775. otherUserId,
  1776. true,
  1777. true,
  1778. 0,
  1779. 0,
  1780. LOAD_MESSAGE_LIMIT,
  1781. 0,
  1782. NEWEST_FIRST
  1783. );
  1784. } else {
  1785. // Self conversation.
  1786. resetNoConversationPromise = Repository.getSelfConversation(
  1787. loggedInUserProfile.id,
  1788. LOAD_MESSAGE_LIMIT,
  1789. 0,
  1790. NEWEST_FIRST
  1791. );
  1792. }
  1793. return resetNoConversationPromise.then(function(conversation) {
  1794. // Looks like we have a conversation after all! Let's use that.
  1795. return resetByConversation(body, conversation, loggedInUserProfile);
  1796. })
  1797. .catch(function() {
  1798. // Can't find a conversation. Oh well. Just load up a blank one.
  1799. return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);
  1800. });
  1801. };
  1802. /**
  1803. * Load new messages into the conversation based on a time interval.
  1804. *
  1805. * @param {Object} body Conversation body container element.
  1806. * @param {Number} conversationId The conversation id.
  1807. * @param {Object} loggedInUserProfile The logged in user's profile.
  1808. * @return {Promise} Renderer promise.
  1809. */
  1810. var resetById = function(body, conversationId, loggedInUserProfile) {
  1811. var cache = null;
  1812. if (conversationId in stateCache) {
  1813. cache = stateCache[conversationId];
  1814. }
  1815. // Always reset the state back to the initial state so that the
  1816. // state manager and patcher can work correctly.
  1817. resetState(body, conversationId, loggedInUserProfile);
  1818. var promise = $.Deferred().resolve({}).promise();
  1819. if (cache) {
  1820. // We've seen this conversation before so there is no need to
  1821. // send any network requests.
  1822. var newState = cache.state;
  1823. // Reset some loading states just in case they were left weirdly.
  1824. newState = StateManager.setLoadingMessages(newState, false);
  1825. newState = StateManager.setLoadingMembers(newState, false);
  1826. setMessagesOffset(cache.messagesOffset);
  1827. setLoadedAllMessages(cache.loadedAllMessages);
  1828. render(newState);
  1829. } else {
  1830. promise = loadNewConversation(
  1831. conversationId,
  1832. loggedInUserProfile,
  1833. LOAD_MESSAGE_LIMIT,
  1834. 0,
  1835. NEWEST_FIRST
  1836. );
  1837. }
  1838. return promise.then(function() {
  1839. return resetMessagePollTimer(conversationId);
  1840. });
  1841. };
  1842. /**
  1843. * Load new messages into the conversation based on a time interval.
  1844. *
  1845. * @param {Object} body Conversation body container element.
  1846. * @param {Object} conversation The conversation.
  1847. * @param {Object} loggedInUserProfile The logged in user's profile.
  1848. * @return {Promise} Renderer promise.
  1849. */
  1850. var resetByConversation = function(body, conversation, loggedInUserProfile) {
  1851. var cache = null;
  1852. if (conversation.id in stateCache) {
  1853. cache = stateCache[conversation.id];
  1854. }
  1855. // Always reset the state back to the initial state so that the
  1856. // state manager and patcher can work correctly.
  1857. resetState(body, conversation.id, loggedInUserProfile);
  1858. var promise = $.Deferred().resolve({}).promise();
  1859. if (cache) {
  1860. // We've seen this conversation before so there is no need to
  1861. // send any network requests.
  1862. var newState = cache.state;
  1863. // Reset some loading states just in case they were left weirdly.
  1864. newState = StateManager.setLoadingMessages(newState, false);
  1865. newState = StateManager.setLoadingMembers(newState, false);
  1866. setMessagesOffset(cache.messagesOffset);
  1867. setLoadedAllMessages(cache.loadedAllMessages);
  1868. render(newState);
  1869. } else {
  1870. promise = loadExistingConversation(
  1871. conversation,
  1872. loggedInUserProfile,
  1873. LOAD_MESSAGE_LIMIT,
  1874. NEWEST_FIRST
  1875. );
  1876. }
  1877. return promise.then(function() {
  1878. return resetMessagePollTimer(conversation.id);
  1879. });
  1880. };
  1881. /**
  1882. * Setup the conversation page. This is a rather complex function because there are a
  1883. * few combinations of arguments that can be provided to this function to show the
  1884. * conversation.
  1885. *
  1886. * There are:
  1887. * 1.) A conversation object with no action or other user id (e.g. from the overview page)
  1888. * 2.) A conversation id with no action or other user id (e.g. from the contacts page)
  1889. * 3.) No conversation/id with an action and other other user id. (e.g. from contact page)
  1890. *
  1891. * @param {string} namespace The route namespace.
  1892. * @param {Object} header Conversation header container element.
  1893. * @param {Object} body Conversation body container element.
  1894. * @param {Object} footer Conversation footer container element.
  1895. * @param {Object|Number|null} conversationOrId Conversation or id or null
  1896. * @param {String} action An action to take on the conversation
  1897. * @param {Number} otherUserId The other user id for a private conversation
  1898. * @return {Object} jQuery promise
  1899. */
  1900. var show = function(namespace, header, body, footer, conversationOrId, action, otherUserId) {
  1901. var conversation = null;
  1902. var conversationId = null;
  1903. // Check what we were given to identify the conversation.
  1904. if (conversationOrId && conversationOrId !== null && typeof conversationOrId == 'object') {
  1905. conversation = conversationOrId;
  1906. conversationId = parseInt(conversation.id, 10);
  1907. } else {
  1908. conversation = null;
  1909. conversationId = parseInt(conversationOrId, 10);
  1910. conversationId = isNaN(conversationId) ? null : conversationId;
  1911. }
  1912. if (!conversationId && action && otherUserId) {
  1913. // If we didn't get a conversation id got a user id then let's see if we've
  1914. // previously loaded a private conversation with this user.
  1915. conversationId = getCachedPrivateConversationIdFromUserId(otherUserId);
  1916. }
  1917. // This is a new conversation if:
  1918. // 1. We don't already have a state
  1919. // 2. The given conversation doesn't match the one currently loaded
  1920. // 3. We have a view state without a conversation id and we weren't given one
  1921. // but we were given a different other user id. This happens when the user
  1922. // goes from viewing a user that they haven't yet initialised a conversation
  1923. // with to viewing a different user that they also haven't initialised a
  1924. // conversation with.
  1925. var isNewConversation = !viewState || (viewState.id != conversationId) || (otherUserId && otherUserId != getOtherUserId());
  1926. if (!body.attr('data-init')) {
  1927. // Generate the render function to bind the header, body, and footer
  1928. // elements to it so that we don't need to pass them around this module.
  1929. render = generateRenderFunction(header, body, footer, isNewConversation);
  1930. registerEventListeners(namespace, header, body, footer);
  1931. body.attr('data-init', true);
  1932. }
  1933. if (isNewConversation) {
  1934. var renderPromise = null;
  1935. var loggedInUserProfile = getLoggedInUserProfile(body);
  1936. if (conversation) {
  1937. renderPromise = resetByConversation(body, conversation, loggedInUserProfile, otherUserId);
  1938. } else if (conversationId) {
  1939. renderPromise = resetById(body, conversationId, loggedInUserProfile, otherUserId);
  1940. } else {
  1941. renderPromise = resetNoConversation(body, loggedInUserProfile, otherUserId);
  1942. }
  1943. return renderPromise
  1944. .then(function() {
  1945. isResetting = false;
  1946. // Focus the first element that can receieve it in the header.
  1947. header.find(Constants.SELECTORS.CAN_RECEIVE_FOCUS).first().focus();
  1948. return;
  1949. })
  1950. .catch(function(error) {
  1951. isResetting = false;
  1952. Notification.exception(error);
  1953. });
  1954. }
  1955. // We're not loading a new conversation so we should reset the poll timer to try to load
  1956. // new messages.
  1957. resetMessagePollTimer(conversationId);
  1958. if (viewState.type == CONVERSATION_TYPES.PRIVATE && action) {
  1959. // There are special actions that the user can perform in a private (aka 1-to-1)
  1960. // conversation.
  1961. var currentOtherUserId = getOtherUserId();
  1962. switch (action) {
  1963. case 'block':
  1964. return requestBlockUser(currentOtherUserId);
  1965. case 'unblock':
  1966. return requestUnblockUser(currentOtherUserId);
  1967. case 'add-contact':
  1968. return requestAddContact(currentOtherUserId);
  1969. case 'remove-contact':
  1970. return requestRemoveContact(currentOtherUserId);
  1971. }
  1972. }
  1973. // Final fallback to return a promise if we didn't need to do anything.
  1974. return $.Deferred().resolve().promise();
  1975. };
  1976. /**
  1977. * String describing this page used for aria-labels.
  1978. *
  1979. * @return {Object} jQuery promise
  1980. */
  1981. var description = function() {
  1982. return Str.get_string('messagedrawerviewconversation', 'core_message', viewState.name);
  1983. };
  1984. return {
  1985. show: show,
  1986. description: description
  1987. };
  1988. });