MDL-63303 message: fix bugs in message drawer part 4
[moodle.git] / message / amd / src / message_drawer_view_search.js
blob5fa0ae82e02b4c31d355afdd089bca46f5b9a826
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.
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/>.
16 /**
17  * Controls the search page of the message drawer.
18  *
19  * @module     core_message/message_drawer_view_search
20  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
23 define(
25     'jquery',
26     'core/custom_interaction_events',
27     'core/notification',
28     'core/pubsub',
29     'core/str',
30     'core/templates',
31     'core_message/message_repository',
32     'core_message/message_drawer_events',
34 function(
35     $,
36     CustomEvents,
37     Notification,
38     PubSub,
39     Str,
40     Templates,
41     Repository,
42     Events
43 ) {
45     var MESSAGE_SEARCH_LIMIT = 50;
46     var USERS_SEARCH_LIMIT = 50;
47     var USERS_INITIAL_SEARCH_LIMIT = 3;
49     var SELECTORS = {
50         BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]',
51         CANCEL_SEARCH_BUTTON: '[data-action="cancel-search"]',
52         CONTACTS_CONTAINER: '[data-region="contacts-container"]',
53         CONTACTS_LIST: '[data-region="contacts-container"] [data-region="list"]',
54         EMPTY_MESSAGE_CONTAINER: '[data-region="empty-message-container"]',
55         LIST: '[data-region="list"]',
56         LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
57         LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]',
58         MESSAGES_LIST: '[data-region="messages-container"] [data-region="list"]',
59         MESSAGES_CONTAINER: '[data-region="messages-container"]',
60         NON_CONTACTS_CONTAINER: '[data-region="non-contacts-container"]',
61         NON_CONTACTS_LIST: '[data-region="non-contacts-container"] [data-region="list"]',
62         SEARCH_ICON_CONTAINER: '[data-region="search-icon-container"]',
63         SEARCH_ACTION: '[data-action="search"]',
64         SEARCH_INPUT: '[data-region="search-input"]',
65         SEARCH_RESULTS_CONTAINER: '[data-region="search-results-container"]',
66         LOAD_MORE_USERS: '[data-action="load-more-users"]',
67         LOAD_MORE_MESSAGES: '[data-action="load-more-messages"]',
68         BUTTON_TEXT: '[data-region="button-text"]',
69         NO_RESULTS_CONTAINTER: '[data-region="no-results-container"]',
70     };
72     var TEMPLATES = {
73         CONTACTS_LIST: 'core_message/message_drawer_contacts_list',
74         NON_CONTACTS_LIST: 'core_message/message_drawer_non_contacts_list',
75         MESSAGES_LIST: 'core_message/message_drawer_messages_list'
76     };
78     /**
79      * Get the logged in user id.
80      *
81      * @param  {Object} body Search body container element.
82      * @return {Number} User id.
83      */
84     var getLoggedInUserId = function(body) {
85         return body.attr('data-user-id');
86     };
88     /**
89      * Show the no messages container element.
90      *
91      * @param  {Object} body Search body container element.
92      * @return {Object} No messages container element.
93      */
94     var getEmptyMessageContainer = function(body) {
95         return body.find(SELECTORS.EMPTY_MESSAGE_CONTAINER);
96     };
98     /**
99      * Get the search loading icon.
100      *
101      * @param  {Object} header Search header container element.
102      * @return {Object} Loading icon element.
103      */
104     var getLoadingIconContainer = function(header) {
105         return header.find(SELECTORS.LOADING_ICON_CONTAINER);
106     };
108     /**
109      * Get the loading container element.
110      *
111      * @param  {Object} body Search body container element.
112      * @return {Object} Loading container element.
113      */
114     var getLoadingPlaceholder = function(body) {
115         return body.find(SELECTORS.LOADING_PLACEHOLDER);
116     };
118     /**
119      * Get the search icon container.
120      *
121      * @param  {Object} header Search header container element.
122      * @return {Object} Search icon container.
123      */
124     var getSearchIconContainer = function(header) {
125         return header.find(SELECTORS.SEARCH_ICON_CONTAINER);
126     };
128     /**
129      * Get the search input container.
130      *
131      * @param  {Object} header Search header container element.
132      * @return {Object} Search input container.
133      */
134     var getSearchInput = function(header) {
135         return header.find(SELECTORS.SEARCH_INPUT);
136     };
138     /**
139      * Get the search results container.
140      *
141      * @param  {Object} body Search body container element.
142      * @return {Object} Search results container.
143      */
144     var getSearchResultsContainer = function(body) {
145         return body.find(SELECTORS.SEARCH_RESULTS_CONTAINER);
146     };
148     /**
149      * Get the search contacts container.
150      *
151      * @param  {Object} body Search body container element.
152      * @return {Object} Search contacts container.
153      */
154     var getContactsContainer = function(body) {
155         return body.find(SELECTORS.CONTACTS_CONTAINER);
156     };
158     /**
159      * Get the search non contacts container.
160      *
161      * @param  {Object} body Search body container element.
162      * @return {Object} Search non contacts container.
163      */
164     var getNonContactsContainer = function(body) {
165         return body.find(SELECTORS.NON_CONTACTS_CONTAINER);
166     };
168     /**
169      * Get the search messages container.
170      *
171      * @param  {Object} body Search body container element.
172      * @return {Object} Search messages container.
173      */
174     var getMessagesContainer = function(body) {
175         return body.find(SELECTORS.MESSAGES_CONTAINER);
176     };
179     /**
180      * Show the messages empty container.
181      *
182      * @param {Object} body Search body container element.
183      */
184     var showEmptyMessage = function(body) {
185         getEmptyMessageContainer(body).removeClass('hidden');
186     };
188     /**
189      * Hide the messages empty container.
190      *
191      * @param {Object} body Search body container element.
192      */
193     var hideEmptyMessage = function(body) {
194         getEmptyMessageContainer(body).addClass('hidden');
195     };
198     /**
199      * Show the loading icon.
200      *
201      * @param {Object} header Search header container element.
202      */
203     var showLoadingIcon = function(header) {
204         getLoadingIconContainer(header).removeClass('hidden');
205     };
207     /**
208      * Hide the loading icon.
209      *
210      * @param {Object} header Search header container element.
211      */
212     var hideLoadingIcon = function(header) {
213         getLoadingIconContainer(header).addClass('hidden');
214     };
216     /**
217      * Show loading placeholder.
218      *
219      * @param {Object} body Search body container element.
220      */
221     var showLoadingPlaceholder = function(body) {
222         getLoadingPlaceholder(body).removeClass('hidden');
223     };
225     /**
226      * Hide loading placeholder.
227      *
228      * @param {Object} body Search body container element.
229      */
230     var hideLoadingPlaceholder = function(body) {
231         getLoadingPlaceholder(body).addClass('hidden');
232     };
234     /**
235      * Show search icon.
236      *
237      * @param {Object} header Search header container element.
238      */
239     var showSearchIcon = function(header) {
240         getSearchIconContainer(header).removeClass('hidden');
241     };
243     /**
244      * Hide search icon.
245      *
246      * @param {Object} header Search header container element.
247      */
248     var hideSearchIcon = function(header) {
249         getSearchIconContainer(header).addClass('hidden');
250     };
252     /**
253      * Show search results.
254      *
255      * @param {Object} body Search body container element.
256      */
257     var showSearchResults = function(body) {
258         getSearchResultsContainer(body).removeClass('hidden');
259     };
261     /**
262      * Hide search results.
263      *
264      * @param {Object} body Search body container element.
265      */
266     var hideSearchResults = function(body) {
267         getSearchResultsContainer(body).addClass('hidden');
268     };
270     /**
271      * Disable the search input.
272      *
273      * @param {Object} header Search header container element.
274      */
275     var disableSearchInput = function(header) {
276         getSearchInput(header).prop('disabled', true);
277     };
279     /**
280      * Enable the search input.
281      *
282      * @param {Object} header Search header container element.
283      */
284     var enableSearchInput = function(header) {
285         getSearchInput(header).prop('disabled', false);
286     };
288     /**
289      * Clear the search input.
290      *
291      * @param {Object} header Search header container element.
292      */
293     var clearSearchInput = function(header) {
294         getSearchInput(header).val('');
295     };
297     /**
298      * Clear all search results
299      *
300      * @param {Object} body Search body container element.
301      */
302     var clearAllSearchResults = function(body) {
303         body.find(SELECTORS.CONTACTS_LIST).empty();
304         body.find(SELECTORS.NON_CONTACTS_LIST).empty();
305         body.find(SELECTORS.MESSAGES_LIST).empty();
306         body.find(SELECTORS.NO_RESULTS_CONTAINTER).addClass('hidden');
307         showLoadMoreUsersButton(body);
308         showLoadMoreMessagesButton(body);
309     };
311     /**
312      * Update the body and header to indicate the search is loading.
313      *
314      * @param {Object} header Search header container element.
315      * @param {Object} body Search body container element.
316      */
317     var startLoading = function(header, body) {
318         hideSearchIcon(header);
319         hideEmptyMessage(body);
320         hideSearchResults(body);
321         showLoadingIcon(header);
322         showLoadingPlaceholder(body);
323         disableSearchInput(header);
324     };
326     /**
327      * Update the body and header to indicate the search has stopped loading.
328      *
329      * @param {Object} header Search header container element.
330      * @param {Object} body Search body container element.
331      */
332     var stopLoading = function(header, body) {
333         showSearchIcon(header);
334         hideEmptyMessage(body);
335         showSearchResults(body);
336         hideLoadingIcon(header);
337         hideLoadingPlaceholder(body);
338         enableSearchInput(header);
339     };
341     /**
342      * Show the more users loading icon.
343      *
344      * @param {Object} root The more users container element.
345      */
346     var showUsersLoadingIcon = function(root) {
347         var button = root.find(SELECTORS.LOAD_MORE_USERS);
348         button.prop('disabled', true);
349         button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
350         button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
351     };
353     /**
354      * Hide the more users loading icon.
355      *
356      * @param {Object} root The more users container element.
357      */
358     var hideUsersLoadingIcon = function(root) {
359         var button = root.find(SELECTORS.LOAD_MORE_USERS);
360         button.prop('disabled', false);
361         button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
362         button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
363     };
365     /**
366      * Show the load more users button.
367      *
368      * @param {Object} root The users container element.
369      */
370     var showLoadMoreUsersButton = function(root) {
371         root.find(SELECTORS.LOAD_MORE_USERS).removeClass('hidden');
372     };
374     /**
375      * Hide the load more users button.
376      *
377      * @param {Object} root The users container element.
378      */
379     var hideLoadMoreUsersButton = function(root) {
380         root.find(SELECTORS.LOAD_MORE_USERS).addClass('hidden');
381     };
383     /**
384      * Show the messages are loading icon.
385      *
386      * @param {Object} root Messages root element.
387      */
388     var showMessagesLoadingIcon = function(root) {
389         var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
390         button.prop('disabled', true);
391         button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
392         button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
393     };
395     /**
396      * Hide the messages are loading icon.
397      *
398      * @param {Object} root Messages root element.
399      */
400     var hideMessagesLoadingIcon = function(root) {
401         var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
402         button.prop('disabled', false);
403         button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
404         button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
405     };
407     /**
408      * Show the load more messages button.
409      *
410      * @param  {Object} root The messages container element.
411      */
412     var showLoadMoreMessagesButton = function(root) {
413         root.find(SELECTORS.LOAD_MORE_MESSAGES).removeClass('hidden');
414     };
416     /**
417      * Hide the load more messages button.
418      *
419      * @param  {Object} root The messages container element.
420      */
421     var hideLoadMoreMessagesButton = function(root) {
422         root.find(SELECTORS.LOAD_MORE_MESSAGES).addClass('hidden');
423     };
425     /**
426      * Find a contact in the search results.
427      *
428      * @param  {Object} root Search results container element.
429      * @param  {Number} userId User id.
430      * @return {Object} User container element.
431      */
432     var findContact = function(root, userId) {
433         return root.find('[data-contact-user-id="' + userId + '"]');
434     };
436     /**
437      * Add a contact to the search results.
438      *
439      * @param {Object} root Search results container.
440      * @param {Object} contact User in contacts list.
441      */
442     var addContact = function(root, contact) {
443         var nonContactsContainer = getNonContactsContainer(root);
444         var nonContact = findContact(nonContactsContainer, contact.userid);
446         if (nonContact.length) {
447             nonContact.remove();
448             var contactsContainer = getContactsContainer(root);
449             contactsContainer.removeClass('hidden');
450             contactsContainer.find(SELECTORS.LIST).append(nonContact);
451         }
453         if (!nonContactsContainer.find(SELECTORS.LIST).children().length) {
454             nonContactsContainer.addClass('hidden');
455         }
456     };
458     /**
459      * Remove a contact from the contacts results.
460      *
461      * @param {Object} root Search results container.
462      * @param {Object} userId Contact user id.
463      */
464     var removeContact = function(root, userId) {
465         var contactsContainer = getContactsContainer(root);
466         var contact = findContact(contactsContainer, userId);
468         if (contact.length) {
469             contact.remove();
470             var nonContactsContainer = getNonContactsContainer(root);
471             nonContactsContainer.removeClass('hidden');
472             nonContactsContainer.find(SELECTORS.LIST).append(contact);
473         }
475         if (!contactsContainer.find(SELECTORS.LIST).children().length) {
476             contactsContainer.addClass('hidden');
477         }
478     };
480     /**
481      * Show the contact is blocked icon.
482      *
483      * @param {Object} root Search results container.
484      * @param {Object} userId Contact user id.
485      */
486     var blockContact = function(root, userId) {
487         var contact = findContact(root, userId);
488         if (contact.length) {
489             contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden');
490         }
491     };
493     /**
494      * Hide the contact is blocked icon.
495      *
496      * @param {Object} root Search results container.
497      * @param {Object} userId Contact user id.
498      */
499     var unblockContact = function(root, userId) {
500         var contact = findContact(root, userId);
501         if (contact.length) {
502             contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden');
503         }
504     };
506     /**
507      * Render contacts in the contacts search results.
508      *
509      * @param {Object} root Search results container.
510      * @param {Array} contacts List of contacts.
511      * @return {Promise} Renderer promise.
512      */
513     var renderContacts = function(root, contacts) {
514         var container = getContactsContainer(root);
515         var list = container.find(SELECTORS.LIST);
517         if (!contacts.length && !list.children().length) {
518             var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
519             noResultsContainer.removeClass('hidden');
520             return $.Deferred().resolve('').promise();
521         } else {
522             return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts})
523                 .then(function(html) {
524                     list.append(html);
525                     return html;
526                 });
527         }
528     };
530     /**
531      * Render non contacts in the contacts search results.
532      *
533      * @param {Object} root Search results container.
534      * @param {Array} nonContacts List of non contacts.
535      * @return {Promise} Renderer promise.
536      */
537     var renderNonContacts = function(root, nonContacts) {
538         var container = getNonContactsContainer(root);
539         var list = container.find(SELECTORS.LIST);
541         if (!nonContacts.length && !list.children().length) {
542             var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
543             noResultsContainer.removeClass('hidden');
544             return $.Deferred().resolve('').promise();
545         } else {
546             return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts})
547                 .then(function(html) {
548                     list.append(html);
549                     return html;
550                 });
551         }
552     };
554     /**
555      * Render messages in the messages search results.
556      *
557      * @param {Object} root Search results container.
558      * @param {Array} messages List of messages.
559      * @return {Promise} Renderer promise.
560      */
561     var renderMessages = function(root, messages) {
562         var container = getMessagesContainer(root);
563         var list = container.find(SELECTORS.LIST);
565         if (!messages.length && !list.children().length) {
566             var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
567             noResultsContainer.removeClass('hidden');
568             return $.Deferred().resolve('').promise();
569         } else {
570             return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages})
571                 .then(function(html) {
572                     list.append(html);
573                     return html;
574                 });
575         }
576     };
578     /**
579      * Load more users from the repository and render the results into the users search results.
580      *
581      * @param  {Object} root Search results container.
582      * @param  {Number} loggedInUserId Current logged in user.
583      * @param  {String} text Search text.
584      * @param  {Number} limit Number of users to get.
585      * @param  {Number} offset Load users from
586      * @return {Object} jQuery promise
587      */
588     var loadMoreUsers = function(root, loggedInUserId, text, limit, offset) {
589         var loadedAll = false;
590         showUsersLoadingIcon(root);
591         return Repository.searchUsers(loggedInUserId, text, limit + 1, offset)
592             .then(function(results) {
593                 var contacts = results.contacts;
594                 var noncontacts = results.noncontacts;
596                 if (contacts.length <= limit && noncontacts.length <= limit) {
597                     loadedAll = true;
598                     return {
599                         contacts: contacts,
600                         noncontacts: noncontacts
601                     };
602                 } else {
603                     return {
604                         contacts: contacts.slice(0, limit),
605                         noncontacts: noncontacts.slice(0, limit)
606                     };
607                 }
608             })
609             .then(function(results) {
610                 return $.when(
611                     renderContacts(root, results.contacts),
612                     renderNonContacts(root, results.noncontacts)
613                 );
614             })
615             .then(function() {
616                 hideUsersLoadingIcon(root);
618                 if (loadedAll) {
619                     hideLoadMoreUsersButton(root);
620                 }
622                 return;
623             })
624             .catch(function(error) {
625                 hideUsersLoadingIcon(root);
626                 // Rethrow error for other handlers.
627                 throw error;
628             });
629     };
631     /**
632      * Load more messages from the repository and render the results into the messages search results.
633      *
634      * @param  {Object} root Search results container.
635      * @param  {Number} loggedInUserId Current logged in user.
636      * @param  {String} text Search text.
637      * @param  {Number} limit Number of messages to get.
638      * @param  {Number} offset Load messages from
639      * @return {Object} jQuery promise
640      */
641     var loadMoreMessages = function(root, loggedInUserId, text, limit, offset) {
642         var loadedAll = false;
643         showMessagesLoadingIcon(root);
644         return Repository.searchMessages(loggedInUserId, text, limit + 1, offset)
645             .then(function(results) {
646                 var messages = results.contacts;
648                 if (messages.length <= limit) {
649                     loadedAll = true;
650                     return messages;
651                 } else {
652                     return messages.slice(0, limit);
653                 }
654             })
655             .then(function(messages) {
656                 return renderMessages(root, messages);
657             })
658             .then(function() {
659                 hideMessagesLoadingIcon(root);
661                 if (loadedAll) {
662                     hideLoadMoreMessagesButton(root);
663                 }
665                 return;
666             })
667             .catch(function(error) {
668                 hideMessagesLoadingIcon(root);
669                 // Rethrow error for other handlers.
670                 throw error;
671             });
672     };
674     /**
675      * Search for users and messages.
676      *
677      * @param {Object} header Search header container element.
678      * @param {Object} body Search body container element.
679      * @param {String} searchText Search text.
680      * @param {Number} usersLimit The users limit.
681      * @param {Number} usersOffset The users offset.
682      * @param {Number} messagesLimit The message limit.
683      * @param {Number} messagesOffset The message offset.
684      * @return {Object} jQuery promise
685      */
686     var search = function(header, body, searchText, usersLimit, usersOffset, messagesLimit, messagesOffset) {
687         var loggedInUserId = getLoggedInUserId(body);
688         startLoading(header, body);
689         clearAllSearchResults(body);
691         return $.when(
692             loadMoreUsers(body, loggedInUserId, searchText, usersLimit, usersOffset),
693             loadMoreMessages(body, loggedInUserId, searchText, messagesLimit, messagesOffset)
694         )
695         .then(function() {
696             stopLoading(header, body);
697             return;
698         });
699     };
702     /**
703      * Listen to and handle events for searching.
704      *
705      * @param {Object} header Search header container element.
706      * @param {Object} body Search body container element.
707      */
708     var registerEventListeners = function(header, body) {
709         var loggedInUserId = getLoggedInUserId(body);
710         var searchInput = getSearchInput(header);
711         var searchText = '';
712         var messagesOffset = 0;
713         var usersOffset = 0;
715         var searchEventHandler = function(e, data) {
716             searchText = searchInput.val().trim();
718             if (searchText !== '') {
719                 messagesOffset = 0;
720                 usersOffset = 0;
721                 search(
722                     header,
723                     body,
724                     searchText,
725                     USERS_INITIAL_SEARCH_LIMIT,
726                     usersOffset,
727                     MESSAGE_SEARCH_LIMIT,
728                     messagesOffset
729                 )
730                 .then(function() {
731                     searchInput.focus();
732                     usersOffset = usersOffset + USERS_INITIAL_SEARCH_LIMIT;
733                     messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
734                     return;
735                 })
736                 .catch(Notification.exception);
737             }
739             data.originalEvent.preventDefault();
740         };
742         CustomEvents.define(searchInput, [CustomEvents.events.enter]);
743         CustomEvents.define(header, [CustomEvents.events.activate]);
744         CustomEvents.define(body, [CustomEvents.events.activate]);
746         searchInput.on(CustomEvents.events.enter, searchEventHandler);
748         header.on(CustomEvents.events.activate, SELECTORS.SEARCH_ACTION, searchEventHandler);
750         body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_MESSAGES, function(e, data) {
751             if (searchText !== '') {
752                 loadMoreMessages(body, loggedInUserId, searchText, MESSAGE_SEARCH_LIMIT, messagesOffset)
753                     .then(function() {
754                         messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
755                         return;
756                     })
757                     .catch(Notification.exception);
758             }
759             data.originalEvent.preventDefault();
760         });
762         body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_USERS, function(e, data) {
763             if (searchText !== '') {
764                 loadMoreUsers(body, loggedInUserId, searchText, USERS_SEARCH_LIMIT, usersOffset)
765                     .then(function() {
766                         usersOffset = usersOffset + USERS_SEARCH_LIMIT;
767                         return;
768                     })
769                     .catch(Notification.exception);
770             }
771             data.originalEvent.preventDefault();
772         });
774         header.on(CustomEvents.events.activate, SELECTORS.CANCEL_SEARCH_BUTTON, function() {
775             clearSearchInput(header);
776             showEmptyMessage(body);
777             showSearchIcon(header);
778             hideSearchResults(body);
779             hideLoadingIcon(header);
780             hideLoadingPlaceholder(body);
781             usersOffset = 0;
782             messagesOffset = 0;
783         });
785         PubSub.subscribe(Events.CONTACT_ADDED, function(userId) {
786             addContact(body, userId);
787         });
789         PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) {
790             removeContact(body, userId);
791         });
793         PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) {
794             blockContact(body, userId);
795         });
797         PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) {
798             unblockContact(body, userId);
799         });
800     };
802     /**
803      * Setup the search page.
804      *
805      * @param {Object} header Contacts header container element.
806      * @param {Object} body Contacts body container element.
807      * @return {Object} jQuery promise
808      */
809     var show = function(header, body) {
810         if (!body.attr('data-init')) {
811             registerEventListeners(header, body);
812             body.attr('data-init', true);
813         }
815         var searchInput = getSearchInput(header);
816         searchInput.focus();
818         return $.Deferred().resolve().promise();
819     };
821     /**
822      * String describing this page used for aria-labels.
823      *
824      * @param {Object} header Contacts header container element.
825      * @return {Object} jQuery promise
826      */
827     var description = function(header) {
828         var searchInput = getSearchInput(header);
829         var searchText = searchInput.val().trim();
830         return Str.get_string('messagedrawerviewsearch', 'core_message', searchText);
831     };
833     return {
834         show: show,
835         description: description
836     };