message/amd/src/message_drawer_view_overview.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 overview page of the message drawer.
  17. *
  18. * @module core_message/message_drawer_view_overview
  19. * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. define(
  23. [
  24. 'jquery',
  25. 'core/key_codes',
  26. 'core/pubsub',
  27. 'core/str',
  28. 'core_message/message_drawer_router',
  29. 'core_message/message_drawer_routes',
  30. 'core_message/message_drawer_events',
  31. 'core_message/message_drawer_view_overview_section',
  32. 'core_message/message_repository',
  33. 'core_message/message_drawer_view_conversation_constants'
  34. ],
  35. function(
  36. $,
  37. KeyCodes,
  38. PubSub,
  39. Str,
  40. Router,
  41. Routes,
  42. MessageDrawerEvents,
  43. Section,
  44. MessageRepository,
  45. Constants
  46. ) {
  47. var SELECTORS = {
  48. CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]',
  49. FAVOURITES: '[data-region="view-overview-favourites"]',
  50. GROUP_MESSAGES: '[data-region="view-overview-group-messages"]',
  51. MESSAGES: '[data-region="view-overview-messages"]',
  52. SEARCH_INPUT: '[data-region="view-overview-search-input"]',
  53. SECTION_TOGGLE_BUTTON: '[data-toggle]'
  54. };
  55. // Categories displayed in the message drawer. Some methods (such as filterCountsByType) are expecting their value
  56. // will be the same as the defined in the CONVERSATION_TYPES, except for the favourite.
  57. var OVERVIEW_SECTION_TYPES = {
  58. PRIVATE: [Constants.CONVERSATION_TYPES.PRIVATE, Constants.CONVERSATION_TYPES.SELF],
  59. PUBLIC: [Constants.CONVERSATION_TYPES.PUBLIC],
  60. FAVOURITE: null
  61. };
  62. var loadAllCountsPromise = null;
  63. /**
  64. * Load the total and unread conversation counts from the server for this user. This function
  65. * returns a jQuery promise that will be resolved with the counts.
  66. *
  67. * The request is only sent once per page load and will be cached for subsequent
  68. * calls to this function.
  69. *
  70. * @param {Number} loggedInUserId The logged in user's id
  71. * @return {Object} jQuery promise
  72. */
  73. var loadAllCounts = function(loggedInUserId) {
  74. if (loadAllCountsPromise === null) {
  75. loadAllCountsPromise = MessageRepository.getAllConversationCounts(loggedInUserId);
  76. }
  77. return loadAllCountsPromise;
  78. };
  79. /**
  80. * Filter a set of counts to return only the count for the given type.
  81. *
  82. * This is used on the result returned by the loadAllCounts function.
  83. *
  84. * @param {Object} counts Conversation counts indexed by conversation type.
  85. * @param {Array|null} types The conversation types handlded by this section (null for all conversation types).
  86. * @param {bool} includeFavourites If this section includes favourites
  87. * @return {Number}
  88. */
  89. var filterCountsByTypes = function(counts, types, includeFavourites) {
  90. var total = 0;
  91. if (types && types.length) {
  92. total = types.reduce(function(carry, type) {
  93. return carry + counts.types[type];
  94. }, total);
  95. }
  96. if (includeFavourites) {
  97. total += counts.favourites;
  98. }
  99. return total;
  100. };
  101. /**
  102. * Opens one of the sections based on whether the section has unread conversations
  103. * or any conversations
  104. *
  105. * Default section priority is favourites, groups, then messages. A section can increase
  106. * in priority if it has conversations in it. It can increase even further if it has
  107. * unread conversations.
  108. *
  109. * @param {Array} sections List of section roots, total counts, and unread counts.
  110. */
  111. var openSection = function(sections) {
  112. var isAlreadyOpen = sections.some(function(section) {
  113. var sectionRoot = section[0];
  114. return Section.isVisible(sectionRoot);
  115. });
  116. if (isAlreadyOpen) {
  117. // The user has already opened a section so there is nothing to do.
  118. return;
  119. }
  120. // Order the sections so that sections with unread conversations are prioritised
  121. // over sections without and sections with total conversations are prioritised
  122. // over sections without.
  123. sections.sort(function(a, b) {
  124. var aTotal = a[1];
  125. var aUnread = a[2];
  126. var bTotal = b[1];
  127. var bUnread = b[2];
  128. if (aUnread > 0 && bUnread == 0) {
  129. return -1;
  130. } else if (aUnread == 0 && bUnread > 0) {
  131. return 1;
  132. } else if (aTotal > 0 && bTotal == 0) {
  133. return -1;
  134. } else if (aTotal == 0 && bTotal > 0) {
  135. return 1;
  136. } else {
  137. return 0;
  138. }
  139. });
  140. // Get the root of the first section after sorting.
  141. var sectionRoot = sections[0][0];
  142. var button = sectionRoot.find(SELECTORS.SECTION_TOGGLE_BUTTON);
  143. // Click it to expand it.
  144. button.click();
  145. };
  146. /**
  147. * Get the search input text element.
  148. *
  149. * @param {Object} header Overview header container element.
  150. * @return {Object} The search input element.
  151. */
  152. var getSearchInput = function(header) {
  153. return header.find(SELECTORS.SEARCH_INPUT);
  154. };
  155. /**
  156. * Get the logged in user id.
  157. *
  158. * @param {Object} body Overview body container element.
  159. * @return {String} Logged in user id.
  160. */
  161. var getLoggedInUserId = function(body) {
  162. return body.attr('data-user-id');
  163. };
  164. /**
  165. * Decrement the contact request count. If the count is zero or below then
  166. * hide the count.
  167. *
  168. * @param {Object} header Conversation header container element.
  169. * @return {Function} A function to handle decrementing the count.
  170. */
  171. var decrementContactRequestCount = function(header) {
  172. return function() {
  173. var countContainer = header.find(SELECTORS.CONTACT_REQUEST_COUNT);
  174. var count = parseInt(countContainer.text(), 10);
  175. count = isNaN(count) ? 0 : count - 1;
  176. if (count <= 0) {
  177. countContainer.addClass('hidden');
  178. } else {
  179. countContainer.text(count);
  180. }
  181. };
  182. };
  183. /**
  184. * Listen to, and handle event in the overview header.
  185. *
  186. * @param {String} namespace Unique identifier for the Routes
  187. * @param {Object} header Conversation header container element.
  188. */
  189. var registerEventListeners = function(namespace, header) {
  190. var searchInput = getSearchInput(header);
  191. var ignoredKeys = [KeyCodes.tab, KeyCodes.shift, KeyCodes.ctrl, KeyCodes.alt];
  192. searchInput.on('click', function() {
  193. Router.go(namespace, Routes.VIEW_SEARCH);
  194. });
  195. searchInput.on('keydown', function(e) {
  196. if (ignoredKeys.indexOf(e.keyCode) < 0 && e.key != 'Meta') {
  197. Router.go(namespace, Routes.VIEW_SEARCH);
  198. }
  199. });
  200. PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header));
  201. PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header));
  202. };
  203. /**
  204. * Setup the overview page.
  205. *
  206. * @param {String} namespace Unique identifier for the Routes
  207. * @param {Object} header Overview header container element.
  208. * @param {Object} body Overview body container element.
  209. * @return {Object} jQuery promise
  210. */
  211. var show = function(namespace, header, body) {
  212. if (!header.attr('data-init')) {
  213. registerEventListeners(namespace, header);
  214. header.attr('data-init', true);
  215. }
  216. var fromPanel = header.attr('data-in-panel') ? 'frompanel' : null;
  217. getSearchInput(header).val('');
  218. var loggedInUserId = getLoggedInUserId(body);
  219. var allCounts = loadAllCounts(loggedInUserId);
  220. var sections = [
  221. // Favourite conversations section.
  222. [body.find(SELECTORS.FAVOURITES), OVERVIEW_SECTION_TYPES.FAVOURITE, true],
  223. // Group conversations section.
  224. [body.find(SELECTORS.GROUP_MESSAGES), OVERVIEW_SECTION_TYPES.PUBLIC, false],
  225. // Private conversations section.
  226. [body.find(SELECTORS.MESSAGES), OVERVIEW_SECTION_TYPES.PRIVATE, false]
  227. ];
  228. sections.forEach(function(args) {
  229. var sectionRoot = args[0];
  230. var sectionTypes = args[1];
  231. var includeFavourites = args[2];
  232. var totalCountPromise = allCounts.then(function(result) {
  233. return filterCountsByTypes(result.total, sectionTypes, includeFavourites);
  234. });
  235. var unreadCountPromise = allCounts.then(function(result) {
  236. return filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
  237. });
  238. Section.show(namespace, null, sectionRoot, null, sectionTypes, includeFavourites,
  239. totalCountPromise, unreadCountPromise, fromPanel);
  240. });
  241. return allCounts.then(function(result) {
  242. var sectionParams = sections.map(function(section) {
  243. var sectionRoot = section[0];
  244. var sectionTypes = section[1];
  245. var includeFavourites = section[2];
  246. var totalCount = filterCountsByTypes(result.total, sectionTypes, includeFavourites);
  247. var unreadCount = filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
  248. return [sectionRoot, totalCount, unreadCount];
  249. });
  250. // Open up one of the sections for the user.
  251. return openSection(sectionParams);
  252. });
  253. };
  254. /**
  255. * String describing this page used for aria-labels.
  256. *
  257. * @return {Object} jQuery promise
  258. */
  259. var description = function() {
  260. return Str.get_string('messagedrawerviewoverview', 'core_message');
  261. };
  262. return {
  263. show: show,
  264. description: description
  265. };
  266. });