diff --git a/lang/en/message.php b/lang/en/message.php index d8052bb1d29..a98112cc088 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -114,6 +114,7 @@ $string['nomessages'] = 'No messages waiting'; $string['nomessagesfound'] = 'No messages were found'; $string['noreply'] = 'Do not reply to this message'; $string['nosearchresults'] = 'There were no results from your search'; +$string['noncontacts'] = 'Non-contacts'; $string['nonotifications'] = 'You have no notifications'; $string['nonewnotifications'] = 'You have no new notifications'; $string['notificationwindow'] = 'Notification window'; @@ -149,6 +150,7 @@ $string['removefromyourcontacts'] = 'Remove from your contacts'; $string['savemysettings'] = 'Save my settings'; $string['search'] = 'Search'; $string['searchforperson'] = 'Search for a person'; +$string['searchforpersonorcourse'] = 'Search for a person or course'; $string['searchmessages'] = 'Search messages'; $string['searchcombined'] = 'Search people and messages'; $string['selectmessagestodelete'] = 'Select messages or conversations to delete'; diff --git a/lib/db/services.php b/lib/db/services.php index eeddebce6c8..f940a24d774 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -651,6 +651,30 @@ $functions = array( 'type' => 'read', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_message_data_for_messagearea_search_messages' => array( + 'classname' => 'core_message_external', + 'methodname' => 'data_for_messagearea_search_messages', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve the template data for searching for messages', + 'type' => 'read', + 'ajax' => true, + ), + 'core_message_data_for_messagearea_search_people' => array( + 'classname' => 'core_message_external', + 'methodname' => 'data_for_messagearea_search_people', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve the template data for searching for people', + 'type' => 'read', + 'ajax' => true, + ), + 'core_message_data_for_messagearea_search_people_in_course' => array( + 'classname' => 'core_message_external', + 'methodname' => 'data_for_messagearea_search_people_in_course', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve the template data for searching for people in a course', + 'type' => 'read', + 'ajax' => true, + ), 'core_message_data_for_messagearea_conversations' => array( 'classname' => 'core_message_external', 'methodname' => 'data_for_messagearea_conversations', diff --git a/message/amd/src/message_area.js b/message/amd/src/message_area.js index 353f2607464..3c565bf39b8 100644 --- a/message/amd/src/message_area.js +++ b/message/amd/src/message_area.js @@ -22,9 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_area_messages', - 'core_message/message_area_profile', 'core_message/message_area_tabs', - 'core_message/message_area_actions'], - function($, Contacts, Messages, Profile, Tabs, Actions) { + 'core_message/message_area_profile', 'core_message/message_area_tabs', 'core_message/message_area_search'], + function($, Contacts, Messages, Profile, Tabs, Search) { /** * Messagearea class. @@ -48,6 +47,7 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar CONVERSATIONS: "[data-region='contacts'][data-region-content='conversations']", DELETEMESSAGES: "[data-action='delete-messages']", DELETEMESSAGECHECKBOX: "[data-region='delete-message-checkbox']", + DELETESEARCHFILTER: "[data-action='search-filter-delete']", LASTMESSAGE: '.lastmessage', LOADINGICON: '.loading-icon', MENU: "[data-region='menu']", @@ -64,8 +64,12 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar PROFILESENDMESSAGE: "[data-action='profile-send-message']", PROFILEUNBLOCKCONTACT: "[data-action='profile-unblock-contact']", PROFILEVIEW: "[data-action='profile-view']", - SEARCHAREA: "[data-region='search-area']", SEARCHBOX: "[data-region='search-box']", + SEARCHFILTER: "[data-region='search-filter']", + SEARCHFILTERAREA: "[data-region='search-filter-area']", + SEARCHPEOPLEINCOURSE : "[data-action='search-people-in-course']", + SEARCHRESULTSAREA: "[data-region='search-results-area']", + SEARCHTEXTAREA: "[data-region='search-text-area']", SELECTEDVIEWPROFILE: "[data-action='view-contact-profile'].selected", SELECTEDVIEWCONVERSATION: "[data-action='view-contact-msg'].selected", SENDMESSAGE: "[data-action='send-message']", @@ -91,7 +95,9 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar CONVERSATIONSELECTED: 'conversation-selected', CONVERSATIONSSELECTED: 'conversations-selected', MESSAGESDELETED: 'messages-deleted', + MESSAGESEARCHCANCELED: 'message-search-canceled', MESSAGESENT: 'message-sent', + PEOPLESEARCHCANCELED: 'people-search-canceled', SENDMESSAGE: 'message-send' }; @@ -106,6 +112,7 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar new Messages(this); new Profile(this); new Tabs(this); + new Search(this); }; /** diff --git a/message/amd/src/message_area_contacts.js b/message/amd/src/message_area_contacts.js index ef11a67c58c..2cb9d7ceb7b 100644 --- a/message/amd/src/message_area_contacts.js +++ b/message/amd/src/message_area_contacts.js @@ -73,6 +73,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust customEvents.events.up, ]); + this.messageArea.onCustomEvent(this.messageArea.EVENTS.MESSAGESEARCHCANCELED, this._viewConversations.bind(this)); + this.messageArea.onCustomEvent(this.messageArea.EVENTS.PEOPLESEARCHCANCELED, this._viewContacts.bind(this)); + this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONTACTSSELECTED, this._viewContacts.bind(this)); this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONSSELECTED, this._viewConversations.bind(this)); this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONTACTSSELECTED, this._viewContacts.bind(this)); this.messageArea.onCustomEvent(this.messageArea.EVENTS.MESSAGESDELETED, this._deleteConversations.bind(this)); @@ -180,7 +183,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust // Replace the text. user.find(this.messageArea.SELECTORS.LASTMESSAGE).empty().append(text); // Ensure user is selected. - this._setSelectedUser(userid); + this._setSelectedUser("[data-userid='" + userid + "']"); }; /** @@ -282,7 +285,15 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust Contacts.prototype._viewConversation = function(event) { if (!this._isDeleting) { var userid = $(event.currentTarget).data('userid'); - this._setSelectedUser(userid); + var messageid = $(event.currentTarget).data('messageid'); + var selector = "[data-userid='" + userid + "']"; + // If we have a specific message id then we did a search and the contact may appear in multiple + // places - we don't want to highlight them all. + if (messageid) { + selector = "[data-messageid='" + messageid + "']"; + } + + this._setSelectedUser(selector); this.messageArea.trigger(this.messageArea.EVENTS.CONVERSATIONSELECTED, userid); // Don't highlight the contact because the message region has changed. this.messageArea.find(this.messageArea.SELECTORS.SELECTEDVIEWPROFILE).removeClass('selected'); @@ -298,7 +309,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust Contacts.prototype._viewContact = function(event) { if (!this._isDeleting) { var userid = $(event.currentTarget).data('userid'); - this._setSelectedUser(userid); + this._setSelectedUser("[data-userid='" + userid + "']"); this.messageArea.trigger(this.messageArea.EVENTS.CONTACTSELECTED, userid); // Don't highlight the conversation because the message region has changed. this.messageArea.find(this.messageArea.SELECTORS.SELECTEDVIEWCONVERSATION).removeClass('selected'); @@ -448,14 +459,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust /** * Handles selecting a contact in the list. * - * @param {int} userid + * @param {String} selector * @private */ - Contacts.prototype._setSelectedUser = function(userid) { + Contacts.prototype._setSelectedUser = function(selector) { // Remove the 'selected' class from any other contact. this.messageArea.find(this.messageArea.SELECTORS.CONTACT).removeClass('selected'); // Set the tab for the user to selected. - this.messageArea.find(this.messageArea.SELECTORS.CONTACT + "[data-userid='" + userid + "']").addClass('selected'); + this.messageArea.find(this.messageArea.SELECTORS.CONTACT + selector).addClass('selected'); }; /** @@ -536,14 +547,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust * Flags the search area as seaching. */ Contacts.prototype._setSearching = function() { - $(this.messageArea.SELECTORS.SEARCHAREA).addClass('searching'); + $(this.messageArea.SELECTORS.SEARCHTEXTAREA).addClass('searching'); }; /** * Flags the search area as seaching. */ Contacts.prototype._clearSearching = function() { - $(this.messageArea.SELECTORS.SEARCHAREA).removeClass('searching'); + $(this.messageArea.SELECTORS.SEARCHTEXTAREA).removeClass('searching'); }; /** diff --git a/message/amd/src/message_area_search.js b/message/amd/src/message_area_search.js new file mode 100644 index 00000000000..b085a06d420 --- /dev/null +++ b/message/amd/src/message_area_search.js @@ -0,0 +1,387 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * The module handles searching contacts. + * + * @module core_message/message_area_search + * @package core_message + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/custom_interaction_events'], + function($, ajax, templates, notification, str, customEvents) { + + /** + * Search class. + * + * @param {Messagearea} messageArea The messaging area object. + */ + function Search(messageArea) { + this.messageArea = messageArea; + this._init(); + } + + /** @type {Messagearea} The messaging area object. */ + Search.prototype.messageArea = null; + + /** @type {String} The area we are searching in. */ + Search.prototype._searchArea = null; + + /** @type {String} The id of the course we are searching in (if any). */ + Search.prototype._courseid = null; + + /** @type {Boolean} checks if we are currently loading */ + Search.prototype._isLoading = false; + + /** @type {String} The number of messages displayed. */ + Search.prototype._numMessagesDisplayed = 0; + + /** @type {String} The number of messages to retrieve. */ + Search.prototype._numMessagesToRetrieve = 20; + + /** @type {String} The number of people displayed. */ + Search.prototype._numPeopleDisplayed = 0; + + /** @type {String} The number of people to retrieve. */ + Search.prototype._numPeopleToRetrieve = 20; + + /** @type {Array} The type of available search areas. **/ + Search.prototype._searchAreas = { + MESSAGES: 'messages', + PEOPLE: 'people', + PEOPLEINCOURSE: 'peopleincourse' + }; + + /** @type {int} The timeout before performing an ajax search */ + Search.prototype._requestTimeout = null; + + /** + * Initialise the event listeners. + * + * @private + */ + Search.prototype._init = function() { + // Handle searching for text. + this.messageArea.find(this.messageArea.SELECTORS.SEARCHTEXTAREA).on('input', this._searchRequest.bind(this)); + + // Handle clicking on a course in the list of people. + this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.SEARCHPEOPLEINCOURSE, function(e) { + this._setFilter($(e.currentTarget).html()); + this._setPlaceholderText('searchforperson'); + this._clearSearchArea(); + this._searchArea = this._searchAreas.PEOPLEINCOURSE; + this._courseid = $(e.currentTarget).data('courseid'); + this._searchPeopleInCourse(); + }.bind(this)); + + // Handle deleting the search filter. + this.messageArea.onDelegateEvent('click', this.messageArea.SELECTORS.DELETESEARCHFILTER, function() { + this._hideSearchResults(); + // Filter has been removed, so we don't want to be searching in a course anymore. + this._searchArea = this._searchAreas.PEOPLE; + this._setPlaceholderText('searchforpersonorcourse'); + // Go back the contacts. + this.messageArea.trigger(this.messageArea.EVENTS.PEOPLESEARCHCANCELED); + }.bind(this)); + + // Handle events that occur outside this module. + this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONSSELECTED, function() { + this._hideSearchResults(); + this._searchArea = this._searchAreas.MESSAGES; + this._setPlaceholderText('searchmessages'); + }.bind(this)); + this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONTACTSSELECTED, function() { + this._hideSearchResults(); + this._searchArea = this._searchAreas.PEOPLE; + this._setPlaceholderText('searchforpersonorcourse'); + }.bind(this)); + this.messageArea.onCustomEvent(this.messageArea.EVENTS.MESSAGESENT, function() { + this._hideSearchResults(); + this._searchArea = this._searchAreas.MESSAGES; + this._setPlaceholderText('searchmessages'); + }.bind(this)); + + // Event listeners for scrolling through messages and people in courses. + customEvents.define(this.messageArea.SELECTORS.SEARCHRESULTSAREA, [ + customEvents.events.scrollBottom + ]); + this.messageArea.onDelegateEvent(customEvents.events.scrollBottom, this.messageArea.SELECTORS.SEARCHRESULTSAREA, + function() { + if (this._searchArea == this._searchAreas.MESSAGES) { + this._searchMessages(); + } else if (this._searchArea == this._searchAreas.PEOPLEINCOURSE) { + this._searchPeopleInCourse(); + } + }.bind(this) + ); + + // Set the initial search area. + this._searchArea = this._searchAreas.MESSAGES; + }; + + /** + * Handles when search requests are sent. + * + * @private + */ + Search.prototype._searchRequest = function() { + var str = this.messageArea.find(this.messageArea.SELECTORS.SEARCHTEXTAREA + ' input').val(); + + if (this._requestTimeout) { + clearTimeout(this._requestTimeout); + } + + if (str.trim() === '') { + // If nothing we being searched then we need to display the usual data. + if (this._searchArea == this._searchAreas.MESSAGES) { + this._hideSearchResults(); + this.messageArea.trigger(this.messageArea.EVENTS.MESSAGESEARCHCANCELED); + } else if (this._searchArea == this._searchAreas.PEOPLE) { + this._hideSearchResults(); + this.messageArea.trigger(this.messageArea.EVENTS.PEOPLESEARCHCANCELED); + } else if (this._searchArea == this._searchAreas.PEOPLEINCOURSE) { + // We are still searching in a course, so need to list all the people again. + this._clearSearchArea(); + this._searchPeopleInCourse(); + } + return; + } + + this.messageArea.find(this.messageArea.SELECTORS.CONVERSATIONS).hide(); + this.messageArea.find(this.messageArea.SELECTORS.CONTACTS).hide(); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHRESULTSAREA).show(); + + if (this._searchArea == this._searchAreas.MESSAGES) { + this._requestTimeout = setTimeout(function () { + this._clearSearchArea(); + this._numMessagesDisplayed = 0; + this._searchMessages(); + }.bind(this), 300); + } else if (this._searchArea == this._searchAreas.PEOPLEINCOURSE) { + this._requestTimeout = setTimeout(function () { + this._clearSearchArea(); + this._numPeopleDisplayed = 0; + this._searchPeopleInCourse(); + }.bind(this), 300); + } else { // Must be searching for people and courses + this._requestTimeout = setTimeout(function() { + this._clearSearchArea(); + this._numPeopleDisplayed = 0; + this._searchPeople(); + }.bind(this), 300); + } + }; + + /** + * Handles searching for messages. + * + * @private + * @returns {Promise} The promise resolved when the search area has been rendered + */ + Search.prototype._searchMessages = function() { + if (this._isLoading) { + return; + } + + var str = this.messageArea.find(this.messageArea.SELECTORS.SEARCHBOX).val(); + + // Tell the user we are loading items. + this._isLoading = true; + + // Call the web service to get our data. + var promises = ajax.call([{ + methodname: 'core_message_data_for_messagearea_search_messages', + args: { + userid: this.messageArea.getCurrentUserId(), + search: str, + limitfrom: this._numMessagesDisplayed, + limitnum: this._numMessagesToRetrieve + } + }]); + + // Keep track of the number of messages + var numberreceived = 0; + // Add loading icon to the end of the list. + return templates.render('core/loading', {}).then(function(html, js) { + templates.appendNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, + "
" + html + "
", js); + return promises[0]; + }.bind(this)).then(function(data) { + numberreceived = data.contacts.length; + return templates.render('core_message/message_area_contacts', data); + }).then(function(html, js) { + // Remove the loading icon. + this.messageArea.find(this.messageArea.SELECTORS.SEARCHRESULTSAREA + " " + + this.messageArea.SELECTORS.LOADINGICON).remove(); + // Only append data if we got data back. + if (numberreceived > 0) { + // Show the new content. + templates.appendNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, html, js); + // Increment the number of contacts displayed. + this._numMessagesDisplayed += numberreceived; + } + // Mark that we are no longer busy loading data. + this._isLoading = false; + }.bind(this)).fail(notification.exception); + }; + + + /** + * Handles searching for people. + * + * @private + * @returns {Promise} The promise resolved when the search area has been rendered + */ + Search.prototype._searchPeople = function() { + var str = this.messageArea.find(this.messageArea.SELECTORS.SEARCHBOX).val(); + + // Call the web service to get our data. + var promises = ajax.call([{ + methodname: 'core_message_data_for_messagearea_search_people', + args: { + userid: this.messageArea.getCurrentUserId(), + search: str, + limitnum: this._numPeopleToRetrieve + } + }]); + + // Perform the search and replace the content. + return templates.render('core/loading', {}).then(function(html, js) { + templates.replaceNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, + "
" + html + "
", js); + return promises[0]; + }.bind(this)).then(function(data) { + return templates.render('core_message/message_area_people_search_results', data); + }).then(function(html, js) { + templates.replaceNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, html, js); + }.bind(this)).fail(notification.exception); + }; + + /** + * Handles searching for people in a course. + * + * @private + * @returns {Promise} The promise resolved when the search area has been rendered + */ + Search.prototype._searchPeopleInCourse = function() { + if (this._isLoading) { + return; + } + + var str = this.messageArea.find(this.messageArea.SELECTORS.SEARCHBOX).val(); + + // Tell the user we are loading items. + this._isLoading = true; + + // Call the web service to get our data. + var promises = ajax.call([{ + methodname: 'core_message_data_for_messagearea_search_people_in_course', + args: { + userid: this.messageArea.getCurrentUserId(), + courseid: this._courseid, + search: str, + limitfrom: this._numPeopleDisplayed, + limitnum: this._numPeopleToRetrieve + } + }]); + + // Keep track of the number of contacts + var numberreceived = 0; + // Add loading icon to the end of the list. + return templates.render('core/loading', {}).then(function(html, js) { + templates.appendNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, + "
" + html + "
", js); + return promises[0]; + }.bind(this)).then(function(data) { + numberreceived = data.contacts.length; + return templates.render('core_message/message_area_people_search_results', data); + }).then(function(html, js) { + // Remove the loading icon. + this.messageArea.find(this.messageArea.SELECTORS.SEARCHRESULTSAREA + " " + + this.messageArea.SELECTORS.LOADINGICON).remove(); + // Only append data if we got data back. + if (numberreceived > 0) { + // Show the new content. + templates.appendNodeContents(this.messageArea.SELECTORS.SEARCHRESULTSAREA, html, js); + // Increment the number of contacts displayed. + this._numPeopleDisplayed += numberreceived; + } + // Mark that we are no longer busy loading data. + this._isLoading = false; + }.bind(this)).fail(notification.exception); + }; + + /** + * Sets placeholder text for search input. + * + * @private + * @param {String} text The placeholder text + * @return {Promise} The promise resolved when the placeholder text has been set + */ + Search.prototype._setPlaceholderText = function(text) { + return str.get_string(text, 'message').then(function(s) { + this.messageArea.find(this.messageArea.SELECTORS.SEARCHTEXTAREA + ' input').attr('placeholder', s); + }.bind(this)); + }; + + + /** + * Sets filter for search input. + * + * @private + * @param {String} text The filter text + */ + Search.prototype._setFilter = function(text) { + this.messageArea.find(this.messageArea.SELECTORS.SEARCHBOX).val(''); + this.messageArea.find(this.messageArea.SELECTORS.CONTACTSAREA).addClass('searchfilter'); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHFILTERAREA).show(); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHFILTER).html(text); + }; + + /** + * Hides filter for search input. + * + * @private + */ + Search.prototype._clearFilters = function() { + this.messageArea.find(this.messageArea.SELECTORS.CONTACTSAREA).removeClass('searchfilter'); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHFILTER).empty(); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHFILTERAREA).hide(); + }; + + /** + * Handles clearing the search area. + * + * @private + */ + Search.prototype._clearSearchArea = function() { + this.messageArea.find(this.messageArea.SELECTORS.SEARCHRESULTSAREA).empty(); + }; + + /** + * Handles hiding the search area. + * + * @private + */ + Search.prototype._hideSearchResults = function() { + this._clearFilters(); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHTEXTAREA + ' input').val(''); + this._clearSearchArea(); + this.messageArea.find(this.messageArea.SELECTORS.SEARCHRESULTSAREA).hide(); + }; + + return Search; +}); \ No newline at end of file diff --git a/message/classes/api.php b/message/classes/api.php index 3f1eb8c05ac..51545defed3 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -36,6 +36,184 @@ defined('MOODLE_INTERNAL') || die(); */ class api { + /** + * Handles searching for messages in the message area. + * + * @param int $userid The user id doing the searching + * @param string $search The string the user is searching + * @param int $limitfrom + * @param int $limitnum + * @return \core_message\output\messagearea\message_search_results + */ + public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) { + global $DB; + + // Get the user fields we want. + $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_'); + $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_'); + + // Get all the messages for the user. + $sql = "SELECT m.id, m.useridfrom, m.useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat, + m.smallmessage, m.notification, m.timecreated, 0 as isread, $ufields, $ufields2 + FROM {message} m + JOIN {user} u + ON m.useridfrom = u.id + JOIN {user} u2 + ON m.useridto = u2.id + WHERE ((useridto = ? AND timeusertodeleted = 0) + OR (useridfrom = ? AND timeuserfromdeleted = 0)) + AND notification = 0 + AND u.deleted = 0 + AND u2.deleted = 0 + AND " . $DB->sql_like('smallmessage', '?', false) . " + UNION ALL + SELECT mr.id, mr.useridfrom, mr.useridto, mr.subject, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat, + mr.smallmessage, mr.notification, mr.timecreated, 1 as isread, $ufields, $ufields2 + FROM {message_read} mr + JOIN {user} u + ON mr.useridfrom = u.id + JOIN {user} u2 + ON mr.useridto = u2.id + WHERE ((useridto = ? AND timeusertodeleted = 0) + OR (useridfrom = ? AND timeuserfromdeleted = 0)) + AND notification = 0 + AND u.deleted = 0 + AND u2.deleted = 0 + AND " . $DB->sql_like('smallmessage', '?', false) . " + ORDER BY timecreated DESC"; + $params = array($userid, $userid, '%' . $search . '%', + $userid, $userid, '%' . $search . '%'); + + // Convert the messages into searchable contacts with their last message being the message that was searched. + $contacts = array(); + if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) { + foreach ($messages as $message) { + $prefix = 'userfrom_'; + if ($userid == $message->useridfrom) { + $prefix = 'userto_'; + // If it from the user, then mark it as read, even if it wasn't by the receiver. + $message->isread = true; + } + $message->messageid = $message->id; + $contacts[] = \core_message\helper::create_contact($message, $prefix); + } + } + + return new \core_message\output\messagearea\message_search_results($userid, $contacts); + } + + /** + * Handles searching for people in a particular course in the message area. + * + * @param int $userid The user id doing the searching + * @param int $courseid The id of the course we are searching in + * @param string $search The string the user is searching + * @param int $limitfrom + * @param int $limitnum + * @return \core_message\output\messagearea\people_search_results + */ + public static function search_people_in_course($userid, $courseid, $search, $limitfrom = 0, $limitnum = 0) { + global $DB; + + // Get all the users in the course. + list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true); + $sql = "SELECT u.* + FROM {user} u + JOIN ($esql) je ON je.id = u.id + WHERE u.deleted = 0"; + // Add more conditions. + $fullname = $DB->sql_fullname(); + $sql .= " AND u.id != :userid + AND " . $DB->sql_like($fullname, ':search', false) . " + ORDER BY " . $DB->sql_fullname(); + $params = array_merge(array('userid' => $userid, 'search' => '%' . $search . '%'), $params); + + + // Convert all the user records into contacts. + $contacts = array(); + if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) { + foreach ($users as $user) { + $contacts[] = \core_message\helper::create_contact($user); + } + } + + return new \core_message\output\messagearea\people_search_results($contacts); + } + + /** + * Handles searching for people in the message area. + * + * @param int $userid The user id doing the searching + * @param string $search The string the user is searching + * @param int $limitnum + * @return \core_message\output\messagearea\people_search_results + */ + public static function search_people($userid, $search, $limitnum = 0) { + global $CFG, $DB; + + require_once($CFG->dirroot . '/lib/coursecatlib.php'); + + // Used to search for contacts. + $fullname = $DB->sql_fullname(); + $ufields = \user_picture::fields('u', array('lastaccess')); + + // Users not to include. + $excludeusers = array($userid, $CFG->siteguest); + list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false); + + // Ok, let's search for contacts first. + $contacts = array(); + $sql = "SELECT $ufields + FROM {user} u + JOIN {message_contacts} mc + ON u.id = mc.contactid + WHERE mc.userid = :userid + AND u.deleted = 0 + AND u.confirmed = 1 + AND " . $DB->sql_like($fullname, ':search', false) . " + AND u.id $exclude + ORDER BY " . $DB->sql_fullname(); + if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'search' => '%' . $search . '%') + + $excludeparams, 0, $limitnum)) { + foreach ($users as $user) { + $contacts[] = \core_message\helper::create_contact($user); + } + } + + // Now, let's get the courses. + $courses = array(); + if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum))) { + foreach ($arrcourses as $course) { + $data = new \stdClass(); + $data->id = $course->id; + $data->shortname = $course->shortname; + $data->fullname = $course->fullname; + $courses[] = $data; + } + } + + // Let's get those non-contacts. Toast them gears boi. + $noncontacts = array(); + $sql = "SELECT $ufields + FROM {user} u + WHERE u.deleted = 0 + AND u.confirmed = 1 + AND " . $DB->sql_like($fullname, ':search', false) . " + AND u.id $exclude + AND u.id NOT IN (SELECT contactid + FROM {message_contacts} + WHERE userid = :userid) + ORDER BY " . $DB->sql_fullname(); + if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'search' => '%' . $search . '%') + + $excludeparams, 0, $limitnum)) { + foreach ($users as $user) { + $noncontacts[] = \core_message\helper::create_contact($user); + } + } + + return new \core_message\output\messagearea\people_search_results($contacts, $courses, $noncontacts); + } + /** * Returns the contacts and their conversation to display in the contacts area. * diff --git a/message/classes/helper.php b/message/classes/helper.php index ba78aa94510..30d46c4afce 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -122,9 +122,10 @@ class helper { * Helper function for creating a contact renderable. * * @param \stdClass $contact + * @param string $prefix * @return \core_message\output\messagearea\contact */ - public static function create_contact($contact) { + public static function create_contact($contact, $prefix = '') { global $CFG, $PAGE; // Variables to check if we consider this user online or not. @@ -135,7 +136,7 @@ class helper { $time = time() - $timetoshowusers; // Create the data we are going to pass to the renderable. - $userfields = \user_picture::unalias($contact, array('lastaccess')); + $userfields = \user_picture::unalias($contact, array('lastaccess'), $prefix . 'id', $prefix); $data = new \stdClass(); $data->userid = $userfields->id; $data->fullname = fullname($userfields); @@ -146,10 +147,13 @@ class helper { $userpicture->size = 0; // Size f2. $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false); // Store the message if we have it. + $data->lastmessage = null; + $data->messageid = null; if (isset($contact->smallmessage)) { $data->lastmessage = $contact->smallmessage; - } else { - $data->lastmessage = null; + if (isset($contact->messageid)) { + $data->messageid = $contact->messageid; + } } // Check if the user is online. $data->isonline = $userfields->lastaccess >= $time; diff --git a/message/classes/output/messagearea/contact.php b/message/classes/output/messagearea/contact.php index dd64793bbfc..8b2cefb59f7 100644 --- a/message/classes/output/messagearea/contact.php +++ b/message/classes/output/messagearea/contact.php @@ -61,6 +61,7 @@ class contact implements templatable, renderable { $contact->fullname = $this->contact->fullname; $contact->profileimageurl = $this->contact->profileimageurl; $contact->profileimageurlsmall = $this->contact->profileimageurlsmall; + $contact->messageid = $this->contact->messageid; if ($this->contact->lastmessage) { $contact->lastmessage = shorten_text($this->contact->lastmessage, self::MAX_MSG_LENGTH); } else { diff --git a/message/classes/output/messagearea/message_search_results.php b/message/classes/output/messagearea/message_search_results.php new file mode 100644 index 00000000000..1dda2ab1699 --- /dev/null +++ b/message/classes/output/messagearea/message_search_results.php @@ -0,0 +1,70 @@ +. + +/** + * Contains class used to display message search results. + * + * @package core_message + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_message\output\messagearea; + +use renderable; +use templatable; + +/** + * Class used to display message search results. + * + * @package core_message + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class message_search_results implements templatable, renderable { + + /** + * The id of the user that the contacts belong to. + */ + protected $userid; + + /** + * The list of contacts with the. + */ + protected $contacts; + + /** + * Constructor. + * + * @param int $userid The id of the user the search results belong to + * @param \core_message\output\messagearea\contact[] $contacts + */ + public function __construct($userid, $contacts) { + $this->userid = $userid; + $this->contacts = $contacts; + } + + public function export_for_template(\renderer_base $output) { + $data = new \stdClass(); + $data->userid = $this->userid; + $data->contacts = array(); + foreach ($this->contacts as $contact) { + $data->contacts[] = $contact->export_for_template($output); + } + + return $data; + } +} \ No newline at end of file diff --git a/message/classes/output/messagearea/people_search_results.php b/message/classes/output/messagearea/people_search_results.php new file mode 100644 index 00000000000..8798a15a88d --- /dev/null +++ b/message/classes/output/messagearea/people_search_results.php @@ -0,0 +1,101 @@ +. + +/** + * Contains class used to display people search results. + * + * @package core_message + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_message\output\messagearea; + +use renderable; +use templatable; + +/** + * Class used to display people search results. + * + * @package core_message + * @copyright 2016 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class people_search_results implements templatable, renderable { + + /** + * The list of contacts + */ + protected $contacts; + + /** + * The list of courses. + */ + protected $courses; + + /** + * The list of non-contacts. + */ + protected $noncontacts; + + /** + * Constructor. + * + * @param \core_message\output\messagearea\contact[] $contacts + * @param array $courses + * @param \core_message\output\messagearea\contact[] $noncontacts + */ + public function __construct($contacts, $courses = array(), $noncontacts = array()) { + $this->contacts = $contacts; + $this->courses = $courses; + $this->noncontacts = $noncontacts; + } + + public function export_for_template(\renderer_base $output) { + // Set the defaults for the data we are going to export. + $data = new \stdClass(); + $data->hascontacts = false; + $data->contacts = array(); + $data->hascourses = false; + $data->courses = array(); + $data->hasnoncontacts = false; + $data->noncontacts = array(); + + // Check if there are any contacts. + if (!empty($this->contacts)) { + $data->hascontacts = true; + foreach ($this->contacts as $contact) { + $data->contacts[] = $contact->export_for_template($output); + } + } + + // Check if there are any courses. + if (!empty($this->courses)) { + $data->hascourses = true; + $data->courses = $this->courses; + } + + // Check if there are any non-contacts. + if (!empty($this->noncontacts)) { + $data->hasnoncontacts = true; + foreach ($this->noncontacts as $noncontact) { + $data->noncontacts[] = $noncontact->export_for_template($output); + } + } + + return $data; + } +} \ No newline at end of file diff --git a/message/externallib.php b/message/externallib.php index bbbfe491fa5..ed3b94861f8 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -420,6 +420,269 @@ class core_message_external extends external_api { return null; } + /** + * Get messagearea search people parameters. + * + * @return external_function_parameters + * @since 3.2 + */ + public static function data_for_messagearea_search_people_in_course_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who is performing the search'), + 'courseid' => new external_value(PARAM_INT, 'The id of the course'), + 'search' => new external_value(PARAM_RAW, 'The string being searched'), + 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * Get messagearea search people results. + * + * @param int $userid The id of the user who is performing the search + * @param int $courseid The id of the course + * @param string $search The string being searched + * @param int $limitfrom + * @param int $limitnum + * @return stdClass + * @throws moodle_exception + * @since 3.2 + */ + public static function data_for_messagearea_search_people_in_course($userid, $courseid, $search, $limitfrom = 0, $limitnum = 0) { + global $CFG, $PAGE; + + // Check if messaging is enabled. + if (!$CFG->messaging) { + throw new moodle_exception('disabled', 'message'); + } + + $params = array( + 'userid' => $userid, + 'courseid' => $courseid, + 'search' => $search, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum + ); + self::validate_parameters(self::data_for_messagearea_search_people_in_course_parameters(), $params); + + self::validate_context(context_user::instance($userid)); + + $search = \core_message\api::search_people_in_course($userid, $courseid, $search, $limitfrom, $limitnum); + + $renderer = $PAGE->get_renderer('core_message'); + return $search->export_for_template($renderer); + } + + /** + * Get messagearea search people returns. + * + * @return external_single_structure + * @since 3.2 + */ + public static function data_for_messagearea_search_people_in_course_returns() { + return new external_single_structure( + array( + 'hascontacts' => new external_value(PARAM_BOOL, 'Are there contacts?'), + 'contacts' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value(PARAM_INT, 'The user\'s id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'), + 'profileimageurl' => new external_value(PARAM_URL, 'User picture URL'), + 'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL'), + 'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'), + 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), + 'isread' => new external_value(PARAM_BOOL, 'If the user has read the message'), + ) + ) + ), + ) + ); + } + + /** + * Get messagearea search people parameters. + * + * @return external_function_parameters + * @since 3.2 + */ + public static function data_for_messagearea_search_people_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who is performing the search'), + 'search' => new external_value(PARAM_RAW, 'The string being searched'), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * Get messagearea search people results. + * + * @param int $userid The id of the user who is performing the search + * @param string $search The string being searched + * @param int $limitnum + * @return stdClass + * @throws moodle_exception + * @since 3.2 + */ + public static function data_for_messagearea_search_people($userid, $search, $limitnum = 0) { + global $CFG, $PAGE; + + // Check if messaging is enabled. + if (!$CFG->messaging) { + throw new moodle_exception('disabled', 'message'); + } + + $params = array( + 'userid' => $userid, + 'search' => $search, + 'limitnum' => $limitnum + ); + self::validate_parameters(self::data_for_messagearea_search_people_parameters(), $params); + + self::validate_context(context_user::instance($userid)); + + $search = \core_message\api::search_people($userid, $search, $limitnum); + + $renderer = $PAGE->get_renderer('core_message'); + return $search->export_for_template($renderer); + } + + /** + * Get messagearea search people returns. + * + * @return external_single_structure + * @since 3.2 + */ + public static function data_for_messagearea_search_people_returns() { + return new external_single_structure( + array( + 'hascontacts' => new external_value(PARAM_BOOL, 'Are there contacts?'), + 'contacts' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value(PARAM_INT, 'The user\'s id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'), + 'profileimageurl' => new external_value(PARAM_URL, 'User picture URL'), + 'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL'), + 'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'), + 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), + 'isread' => new external_value(PARAM_BOOL, 'If the user has read the message'), + ) + ) + ), + 'hascourses' => new external_value(PARAM_BOOL, 'Are there courses?'), + 'courses' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'The course id'), + 'shortname' => new external_value(PARAM_NOTAGS, 'The course shortname'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The course fullname'), + ) + ) + ), + 'hasnoncontacts' => new external_value(PARAM_BOOL, 'Are there non-contacts?'), + 'noncontacts' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value(PARAM_INT, 'The user\'s id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'), + 'profileimageurl' => new external_value(PARAM_URL, 'User picture URL'), + 'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL'), + 'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'), + 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), + 'isread' => new external_value(PARAM_BOOL, 'If the user has read the message'), + ) + ) + ) + ) + ); + } + + /** + * Get messagearea search messages parameters. + * + * @return external_function_parameters + * @since 3.2 + */ + public static function data_for_messagearea_search_messages_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who is performing the search'), + 'search' => new external_value(PARAM_RAW, 'The string being searched'), + 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0) + ) + ); + } + + /** + * Get messagearea search messages results. + * + * @param int $userid The id of the user who is performing the search + * @param string $search The string being searched + * @param int $limitfrom + * @param int $limitnum + * @return stdClass + * @throws moodle_exception + * @since 3.2 + */ + public static function data_for_messagearea_search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) { + global $CFG, $PAGE; + + // Check if messaging is enabled. + if (!$CFG->messaging) { + throw new moodle_exception('disabled', 'message'); + } + + $params = array( + 'userid' => $userid, + 'search' => $search, + 'limitfrom' => $limitfrom, + 'limitnum' => $limitnum + + ); + self::validate_parameters(self::data_for_messagearea_search_messages_parameters(), $params); + + self::validate_context(context_user::instance($userid)); + + $search = \core_message\api::search_messages($userid, $search, $limitfrom, $limitnum); + + $renderer = $PAGE->get_renderer('core_message'); + return $search->export_for_template($renderer); + } + + /** + * Get messagearea search messages returns. + * + * @return external_single_structure + * @since 3.2 + */ + public static function data_for_messagearea_search_messages_returns() { + return new external_single_structure( + array( + 'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'), + 'contacts' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value(PARAM_INT, 'The user\'s id'), + 'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'), + 'profileimageurl' => new external_value(PARAM_URL, 'User picture URL'), + 'profileimageurlsmall' => new external_value(PARAM_URL, 'Small user picture URL'), + 'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'), + 'messageid' => new external_value(PARAM_INT, 'The unique search message id'), + 'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'), + 'isread' => new external_value(PARAM_BOOL, 'If the user has read the message'), + ) + ) + ) + ) + ); + } + /** * The messagearea conversations parameters. * diff --git a/message/templates/message_area_contact.mustache b/message/templates/message_area_contact.mustache index 90078a02596..c403571eee1 100644 --- a/message/templates/message_area_contact.mustache +++ b/message/templates/message_area_contact.mustache @@ -1,4 +1,6 @@ -
+
diff --git a/message/templates/message_area_contacts_area.mustache b/message/templates/message_area_contacts_area.mustache index d6840c01a11..e3badbf2cec 100644 --- a/message/templates/message_area_contacts_area.mustache +++ b/message/templates/message_area_contacts_area.mustache @@ -1,14 +1,19 @@ -
+
- - + +
+
{{> core_message/message_area_contacts }}
-{{! Hidden div to load the other tabs items in via JS when appropriate. }} +{{! Hidden divs to load the other tab and search panels via JS when appropriate. }} +