Merge branch 'MDL-63303-master' of git://github.com/ryanwyllie/moodle

This commit is contained in:
Andrew Nicols
2018-11-15 16:25:20 +08:00
188 changed files with 17754 additions and 1013 deletions
@@ -156,4 +156,49 @@ class user_favourite_service {
]
);
}
/**
* Get the favourite.
*
* @param string $component the frankenstyle component name.
* @param string $itemtype the type of the favourited item.
* @param int $itemid the id of the item which was favourited (not the favourite's id).
* @param \context $context the context of the item which was favourited.
* @return favourite|null
*/
public function get_favourite(string $component, string $itemtype, int $itemid, \context $context) {
try {
return $this->repo->find_favourite(
$this->userid,
$component,
$itemtype,
$itemid,
$context->id
);
} catch (\dml_missing_record_exception $e) {
return null;
}
}
/**
* Count the favourite by item type.
*
* @param string $component the frankenstyle component name.
* @param string $itemtype the type of the favourited item.
* @param \context|null $context the context of the item which was favourited.
* @return favourite|null
*/
public function count_favourites_by_type(string $component, string $itemtype, \context $context = null) {
$criteria = [
'userid' => $this->userid,
'component' => $component,
'itemtype' => $itemtype
];
if ($context) {
$criteria['contextid'] = $context->id;
}
return $this->repo->count_by($criteria);
}
}
+81 -4
View File
@@ -111,7 +111,7 @@ class user_favourite_service_testcase extends advanced_testcase {
return $fakerow;
}
}
throw new \moodle_exception("Item not found");
throw new \dml_missing_record_exception("Item not found");
})
);
$mockrepo->expects($this->any())
@@ -127,16 +127,17 @@ class user_favourite_service_testcase extends advanced_testcase {
})
);
$mockrepo->expects($this->any())
->method('exists_by')
->method('count_by')
->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
$count = 0;
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
foreach ($mockstore as $index => $mockrow) {
$mockrowarr = (array)$mockrow;
if (array_diff($criteria, $mockrowarr) == []) {
return true;
$count++;
}
}
return false;
return $count;
})
);
$mockrepo->expects($this->any())
@@ -149,6 +150,19 @@ class user_favourite_service_testcase extends advanced_testcase {
}
})
);
$mockrepo->expects($this->any())
->method('exists_by')
->will($this->returnCallback(function(array $criteria) use (&$mockstore) {
// Check the mockstore for all objects with properties matching the key => val pairs in $criteria.
foreach ($mockstore as $index => $mockrow) {
$mockrowarr = (array)$mockrow;
if (array_diff($criteria, $mockrowarr) == []) {
return true;
}
}
return false;
})
);
return $mockrepo;
}
@@ -352,4 +366,67 @@ class user_favourite_service_testcase extends advanced_testcase {
)
);
}
/**
* Test confirming the behaviour of the get_favourite() method.
*/
public function test_get_favourite() {
list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
// Get a user_favourite_service for the user.
$repo = $this->get_mock_repository([]);
$service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
// Favourite a course.
$fav1 = $service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
$result = $service->get_favourite(
'core_course',
'course',
$course1context->instanceid,
$course1context
);
// Verify we can get the favourite.
$this->assertEquals($fav1->id, $result->id);
// And one that we know doesn't exist.
$this->assertNull(
$service->get_favourite(
'core_course',
'someothertype',
$course1context->instanceid,
$course1context
)
);
}
/**
* Test confirming the behaviour of the count_favourites_by_type() method.
*/
public function test_count_favourites_by_type() {
list($user1context, $user2context, $course1context, $course2context) = $this->setup_users_and_courses();
// Get a user_favourite_service for the user.
$repo = $this->get_mock_repository([]);
$service = new \core_favourites\local\service\user_favourite_service($user1context, $repo);
$this->assertEquals(0, $service->count_favourites_by_type('core_course', 'course', $course1context));
// Favourite a course.
$service->create_favourite('core_course', 'course', $course1context->instanceid, $course1context);
$this->assertEquals(1, $service->count_favourites_by_type('core_course', 'course', $course1context));
// Favourite another course.
$service->create_favourite('core_course', 'course', $course2context->instanceid, $course1context);
$this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context));
// Favourite a course in another context.
$service->create_favourite('core_course', 'course', $course2context->instanceid, $course2context);
// Doesn't affect original context.
$this->assertEquals(2, $service->count_favourites_by_type('core_course', 'course', $course1context));
// Gets counted if we include all contexts.
$this->assertEquals(3, $service->count_favourites_by_type('core_course', 'course'));
}
}
+53
View File
@@ -22,13 +22,20 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['acceptandaddcontact'] = 'Accept and add to contacts';
$string['addcontact'] = 'Add contact';
$string['addcontactconfirm'] = 'Are you sure you want to add {$a} to your contacts?';
$string['addtoyourcontacts'] = 'Add to your contacts';
$string['addtoyourcontactsandmessage'] = 'Add to your contacts and message';
$string['addtofavourites'] = 'Add to favourites';
$string['ago'] = '{$a} ago';
$string['allusers'] = 'All messages from all users';
$string['backto'] = 'Back to {$a}';
$string['backupmessageshelp'] = 'If enabled, then instant messages will be included in SITE automated backups';
$string['blockcontact'] = 'Block contact';
$string['blockedusers'] = 'Blocked users';
$string['blockuser'] = 'Block user';
$string['blockuserconfirm'] = 'Are you sure you want to block {$a}?';
$string['blocknoncontacts'] = 'Prevent non-contacts from messaging me';
$string['canceledit'] = 'Cancel editing messages';
$string['contactableprivacy'] = 'Accept messages from:';
@@ -37,12 +44,16 @@ $string['contactableprivacy_coursemember'] = 'My contacts and anyone in my cours
$string['contactableprivacy_site'] = 'Anyone on the site';
$string['contactblocked'] = 'Contact blocked';
$string['contactrequests'] = 'Contact requests';
$string['contactrequestsent'] = 'Contact request sent';
$string['contacts'] = 'Contacts';
$string['decline'] = 'Decline';
$string['defaultmessageoutputs'] = 'Default message outputs';
$string['defaults'] = 'Defaults';
$string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation?";
$string['deleteallmessages'] = "Delete all messages";
$string['deleteconversation'] = "Delete conversation";
$string['deleteselectedmessages'] = 'Delete selected messages';
$string['deleteselectedmessagesconfirm'] = 'Are you sure you would like to delete the selected messages?';
$string['disableall'] = 'Disable notifications';
$string['disabled'] = 'Messaging is disabled on this site';
$string['disallowed'] = 'Disallowed';
@@ -64,8 +75,13 @@ $string['eventmessagesent'] = 'Message sent';
$string['forced'] = 'Locked';
$string['guestnoeditmessage'] = 'Guest user can not edit messaging options';
$string['guestnoeditmessageother'] = 'Guest user can not edit other user messaging options';
$string['groupinfo'] = 'Group info';
$string['groupmessages'] = 'Group messages';
$string['hidemessagewindow'] = 'Hide message window';
$string['hidenotificationwindow'] = 'Hide notification window';
$string['info'] = 'Info';
$string['isnotinyourcontacts'] = '{$a} is not in your contacts';
$string['loadmore'] = 'Load more';
$string['loggedin'] = 'Online';
$string['loggedin_help'] = 'Configure how you would like to receive notifications when you are logged into Moodle';
$string['loggedindescription'] = 'When you are logged into Moodle';
@@ -78,24 +94,40 @@ $string['messagepreferences'] = 'Message preferences';
$string['message'] = 'Message';
$string['messagecontactrequestsnotification'] = '{$a} wants to be added as a contact';
$string['messagecontactrequestsnotificationsubject'] = '{$a} wants to be added as a contact';
$string['messagedrawerviewcontact'] = 'User details for {$a}';
$string['messagedrawerviewcontacts'] = 'Message contacts';
$string['messagedrawerviewconversation'] = 'Conversation with {$a}';
$string['messagedrawerviewgroupinfo'] = 'Group details for {$a}';
$string['messagedrawerviewoverview'] = 'Messages overview';
$string['messagedrawerviewsearch'] = 'Messages search results for {$a}';
$string['messagedrawerviewsettings'] = 'Message settings';
$string['messagepreferences'] = 'Message preferences';
$string['messages'] = 'Messages';
$string['messagesselected:'] = 'Messages selected:';
$string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.';
$string['newonlymsg'] = 'Show only new';
$string['newmessage'] = 'New message';
$string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
$string['nofavourites'] = 'No favourites';
$string['noframesjs'] = 'Use more accessible interface';
$string['nocontactrequests'] = 'No contact requests';
$string['nocontacts'] = 'No contacts';
$string['nocontactsgetstarted'] = "Try searching for someone to add them as a contact";
$string['nogroupmessages'] = 'No group messages';
$string['nomessages'] = 'No messages';
$string['nomessagesfound'] = 'No messages were found';
$string['noreply'] = 'Do not reply to this message';
$string['noncontacts'] = 'Non-contacts';
$string['nonotifications'] = 'You have no notifications';
$string['noparticipants'] = 'No participants';
$string['notificationdatahasnotbeenmigrated'] = 'Your notifications are temporarily unavailable due to upgrades in the notification infrastructure. Please wait for them to be migrated.';
$string['notificationwindow'] = 'Notification window';
$string['notificationpreferences'] = 'Notification preferences';
$string['notificationimage'] = 'Notification image';
$string['notifications'] = 'Notifications';
$string['notincontactsheading'] = '{$a} is not in your contacts';
$string['notincontacts'] = 'You need to add {$a} to your contacts to be able to send them messages.';
$string['numparticipants'] = '{$a} participants';
$string['off'] = 'Off';
$string['offline'] = 'Offline';
$string['on'] = 'On';
@@ -105,7 +137,10 @@ $string['outputdoesnotexist'] = 'Message output does not exists';
$string['outputenabled'] = 'Output enabled';
$string['outputnotavailable'] = 'Not available';
$string['outputnotconfigured'] = 'Not configured';
$string['participants'] = 'Participants';
$string['permitted'] = 'Permitted';
$string['privacy'] = 'Privacy';
$string['privacy_desc'] = 'You can restrict who can message you';
$string['privacy:metadata:messages'] = 'Messages';
$string['privacy:metadata:messages:conversationid'] = 'The ID of the conversation';
$string['privacy:metadata:messages:fullmessage'] = 'The full message';
@@ -154,12 +189,18 @@ $string['privacy:metadata:preference:core_message_settings'] = 'Settings related
$string['privacy:request:preference:set'] = 'The value of the setting \'{$a->name}\' was \'{$a->value}\'';
$string['processorsettings'] = 'Processor settings';
$string['removecontact'] = 'Remove contact';
$string['removecontactconfirm'] = 'Are you sure you want to remove {$a} from your contacts?';
$string['removecoursefilter'] = 'Remove filter for course {$a}';
$string['removefromyourcontacts'] = 'Remove from your contacts';
$string['removefromfavourites'] = 'Remove from favourites';
$string['requirecontacttomessage'] = 'You need to request {$a} to add you as a contact to be able to message';
$string['requiresconfiguration'] = 'Requires configuration';
$string['searchforuser'] = 'Search for a user';
$string['searchforuserorcourse'] = 'Search for a user or course';
$string['searchmessages'] = 'Search messages';
$string['searchnocontactsfound'] = 'No contacts found';
$string['searchnomessagesfound'] = 'No messages found';
$string['searchnononcontactsfound'] = 'No non contacts found';
$string['searchcombined'] = 'Search people and messages';
$string['seeall'] = 'See all';
$string['selectmessagestodelete'] = 'Select messages to delete';
@@ -167,6 +208,7 @@ $string['selectnotificationtoview'] = 'Select from the list of notifications on
$string['send'] = 'Send';
$string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
$string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}';
$string['sendcontactrequest'] = 'Send contact request';
$string['sendmessage'] = 'Send message';
$string['sendbulkmessage'] = 'Send message to {$a} people';
$string['sendbulkmessagesingle'] = 'Send message to 1 person';
@@ -182,19 +224,30 @@ $string['shownotificationwindowwithcount'] = 'Show notification window with {$a}
$string['togglenotificationmenu'] = 'Toggle notifications menu';
$string['togglemessagemenu'] = 'Toggle messages menu';
$string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) that doesn\'t exist';
$string['unabletomessage'] = 'You are unable to message this user';
$string['unblock'] = 'Unblock';
$string['unblockcontact'] = 'Unblock contact';
$string['unblockuser'] = 'Unblock user';
$string['unblockuserconfirm'] = 'Are you sure you want to unblock {$a}?';
$string['unknownuser'] = 'Unknown user';
$string['unreadnotification'] = 'Unread notification: {$a}';
$string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
$string['unreadnewmessage'] = 'New message from {$a}';
$string['useentertosend'] = 'Use enter to send';
$string['usercantbemessaged'] = 'You can\'t message {$a} due to their message preferences. Try adding them as a contact.';
$string['userisblockingyou'] = 'This user has blocked you from sending messages to them';
$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
$string['userwouldliketocontactyou'] = '{$a} would like to contact you';
$string['viewfullnotification'] = 'View full notification';
$string['viewinganotherusersmessagearea'] = 'You are viewing another user\'s message area.';
$string['viewmessageswith'] = 'View messages with {$a}';
$string['viewnotificationresource'] = 'Go to: {$a}';
$string['viewunreadmessageswith'] = 'View unread messages with {$a}';
$string['writeamessage'] = 'Write a message...';
$string['wouldliketocontactyou'] = 'Would like to contact you';
$string['you'] = 'You:';
$string['youhaveblockeduser'] = 'You have blocked this user in the past';
$string['yourcontactrequestpending'] = 'Your contact request is pending with {$a}';
// Deprecated since Moodle 3.6.
$string['eventmessagecontactblocked'] = 'Message contact blocked';
+2
View File
@@ -808,6 +808,7 @@ $string['expandcategory'] = 'Expand {$a}';
$string['explanation'] = 'Explanation';
$string['explanationdigitalminor'] = 'This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.';
$string['extendperiod'] = 'Extended period';
$string['favourites'] = 'Favourites';
$string['failedloginattempts'] = '{$a->attempts} failed logins since your last login';
$string['feedback'] = 'Feedback';
$string['file'] = 'File';
@@ -1656,6 +1657,7 @@ $string['requestcourse'] = 'Request a course';
$string['requestedby'] = 'Requested by';
$string['requestedcourses'] = 'Requested courses';
$string['requestreason'] = 'Reason for course request';
$string['requests'] = 'Requests';
$string['required'] = 'Required';
$string['requirespayment'] = 'This course requires payment for access';
$string['resendemail'] = 'Resend email';
+1 -1
View File
@@ -1 +1 @@
define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e,h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i<c?c:d&&i>=d?d:i},e=function(b){var e=a(b.target),f=e.data("min-rows"),g=e.attr("rows");"undefined"==typeof f&&e.data("min-rows",g);var h=d(e);h!=g&&(e.attr("rows",h),e.trigger(c.ROW_CHANGE))},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this)):a(c).on("input propertychange",b.ELEMENT,e.bind(this))};return{init:f,events:c}});
define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e,h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i<c?c:d&&i>=d?d:i},e=function(b){var e=a(b.target),f=e.data("min-rows"),g=e.attr("rows");"undefined"==typeof f&&e.data("min-rows",g),e.attr("rows",1);var h=d(e);e.attr("rows",h),h!=g&&e.trigger(c.ROW_CHANGE)},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this)):a(c).on("input propertychange",b.ELEMENT,e.bind(this))};return{init:f,events:c}});
+1 -1
View File
@@ -1 +1 @@
define(function(){return{tab:9,enter:13,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}});
define(function(){return{tab:9,enter:13,shift:16,ctrl:17,alt:18,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}});
+5 -1
View File
@@ -80,10 +80,14 @@ define(['jquery'], function($) {
if (typeof minRows === "undefined") {
element.data('min-rows', currentRows);
}
// Reset element to single row so that the scroll height of the
// element is correctly calculated each time.
element.attr('rows', 1);
var rows = calculateRows(element);
element.attr('rows', rows);
if (rows != currentRows) {
element.attr('rows', rows);
element.trigger(EVENTS.ROW_CHANGE);
}
};
+3
View File
@@ -28,6 +28,9 @@ define(function() {
return /** @alias module:core/key_codes */ {
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'escape': 27,
'space': 32,
'end': 35,
+3 -3
View File
@@ -185,13 +185,13 @@ XPATH
.//*[self::label or self::div[contains(concat(' ', @class, ' '), ' fstaticlabel ')]][contains(., %locator%)]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
XPATH
, 'message_area_region' => <<<XPATH
.//div[@data-region='messaging-area']/descendant::*[@data-region = %locator%]
.//div[@data-region='message-drawer']/descendant::*[@data-region = %locator%]
XPATH
, 'message_area_region_content' => <<<XPATH
.//div[@data-region='messaging-area']/descendant::*[@data-region-content = %locator%]
.//div[@data-region='message-drawer']/descendant::*[@data-region-content = %locator%]
XPATH
, 'message_area_action' => <<<XPATH
.//div[@data-region='messaging-area']/descendant::*[@data-action = %locator%]
.//div[@data-region='message-drawer']/descendant::*[@data-action = %locator%]
XPATH
, 'autocomplete_selection' => <<<XPATH
.//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='listitem'][contains(normalize-space(.), %locator%)]
@@ -294,6 +294,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/scheduled' => 'fa-calendar-check-o',
'core:i/search' => 'fa-search',
'core:i/section' => 'fa-folder-o',
'core:i/sendmessage' => 'fa-paper-plane',
'core:i/settings' => 'fa-cog',
'core:i/show' => 'fa-eye-slash',
'core:i/siteevent' => 'fa-globe',
@@ -302,6 +303,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/stats' => 'fa-line-chart',
'core:i/switch' => 'fa-exchange',
'core:i/switchrole' => 'fa-user-secret',
'core:i/trash' => 'fa-trash',
'core:i/twoway' => 'fa-arrows-h',
'core:i/unchecked' => 'fa-square-o',
'core:i/unflagged' => 'fa-flag-o',
@@ -312,6 +314,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/users' => 'fa-users',
'core:i/valid' => 'fa-check text-success',
'core:i/warning' => 'fa-exclamation text-warning',
'core:i/window_close' => 'fa-window-close',
'core:i/withsubcat' => 'fa-plus-square',
'core:m/USD' => 'fa-usd',
'core:t/addcontact' => 'fa-address-card',
@@ -332,6 +335,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:t/collapsed_empty' => 'fa-plus-square-o',
'core:t/collapsed_rtl' => 'fa-plus-square',
'core:t/collapsed' => 'fa-plus-square',
'core:t/collapsedcaret' => 'fa-caret-right',
'core:t/contextmenu' => 'fa-cog',
'core:t/copy' => 'fa-copy',
'core:t/delete' => 'fa-trash',
@@ -376,6 +380,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:t/reset' => 'fa-repeat',
'core:t/restore' => 'fa-arrow-circle-up',
'core:t/right' => 'fa-arrow-right',
'core:t/sendmessage' => 'fa-paper-plane',
'core:t/show' => 'fa-eye-slash',
'core:t/sort_asc' => 'fa-sort-asc',
'core:t/sort_desc' => 'fa-sort-desc',
+31
View File
@@ -1086,6 +1086,25 @@ $functions = array(
'description' => 'Retrieve a list of conversations for a user',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true
),
'core_message_get_conversation' => array(
'classname' => 'core_message_external',
'methodname' => 'get_conversation',
'classpath' => 'message/externallib.php',
'description' => 'Retrieve a conversation for a user',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true
),
'core_message_get_conversation_between_users' => array(
'classname' => 'core_message_external',
'methodname' => 'get_conversation_between_users',
'classpath' => 'message/externallib.php',
'description' => 'Retrieve a conversation for a user between another user',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true
),
'core_message_get_messages' => array(
'classname' => 'core_message_external',
@@ -1105,6 +1124,15 @@ $functions = array(
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true,
),
'core_message_get_member_info' => array(
'classname' => 'core_message_external',
'methodname' => 'get_member_info',
'classpath' => 'message/externallib.php',
'description' => 'Retrieve a user message profiles',
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
'ajax' => true,
),
'core_message_get_unread_conversations_count' => array(
'classname' => 'core_message_external',
'methodname' => 'get_unread_conversations_count',
@@ -1257,6 +1285,7 @@ $functions = array(
'classpath' => 'message/externallib.php',
'description' => 'Mark a conversation or group of conversations as favourites/starred conversations.',
'type' => 'write',
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_message_unset_favourite_conversations' => array(
@@ -1265,6 +1294,7 @@ $functions = array(
'classpath' => 'message/externallib.php',
'description' => 'Unset a conversation or group of conversations as favourites/starred conversations.',
'type' => 'write',
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_notes_create_notes' => array(
@@ -1541,6 +1571,7 @@ $functions = array(
'classpath' => 'user/externallib.php',
'description' => 'Return user preferences.',
'type' => 'read',
'ajax' => true,
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_user_update_picture' => array(
+49
View File
@@ -705,6 +705,14 @@ class core_renderer extends renderer_base {
$output .= "\n".$CFG->additionalhtmltopofbody;
}
// Give subsystems an opportunity to inject extra html content. The callback
// must always return a string containing valid html.
foreach (\core_component::get_core_subsystems() as $name => $path) {
if ($path) {
$output .= component_callback($name, 'before_standard_top_of_body_html', [], '');
}
}
// Give plugins an opportunity to inject extra html content. The callback
// must always return a string containing valid html.
$pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
@@ -938,6 +946,39 @@ class core_renderer extends renderer_base {
return $output;
}
/**
* The standard HTML that should be output just before the <footer> tag.
* Designed to be called in theme layout.php files.
*
* @return string HTML fragment.
*/
public function standard_after_main_region_html() {
global $CFG;
$output = '';
if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
$output .= "\n".$CFG->additionalhtmlbottomofbody;
}
// Give subsystems an opportunity to inject extra html content. The callback
// must always return a string containing valid html.
foreach (\core_component::get_core_subsystems() as $name => $path) {
if ($path) {
$output .= component_callback($name, 'standard_after_main_region_html', [], '');
}
}
// Give plugins an opportunity to inject extra html content. The callback
// must always return a string containing valid html.
$pluginswithfunction = get_plugins_with_function('standard_after_main_region_html', 'lib.php');
foreach ($pluginswithfunction as $plugins) {
foreach ($plugins as $function) {
$output .= $function();
}
}
return $output;
}
/**
* Return the standard string that says whether you are logged in (and switched
* roles/logged in as another user).
@@ -3267,6 +3308,14 @@ EOD;
public function navbar_plugin_output() {
$output = '';
// Give subsystems an opportunity to inject extra html content. The callback
// must always return a string containing valid html.
foreach (\core_component::get_core_subsystems() as $name => $path) {
if ($path) {
$output .= component_callback($name, 'render_navbar_output', [$this], '');
}
}
if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginfunction) {
+15 -3
View File
@@ -98,6 +98,8 @@ class moodle_exception extends Exception {
* @param string $debuginfo optional debugging information
*/
function __construct($errorcode, $module='', $link='', $a=NULL, $debuginfo=null) {
global $CFG;
if (empty($module) || $module == 'moodle' || $module == 'core') {
$module = 'error';
}
@@ -116,11 +118,21 @@ class moodle_exception extends Exception {
$haserrorstring = false;
}
if (defined('PHPUNIT_TEST') and PHPUNIT_TEST and $debuginfo) {
$message = "$message ($debuginfo)";
$isinphpunittest = (defined('PHPUNIT_TEST') && PHPUNIT_TEST);
$hasdebugdeveloper = (
isset($CFG->debugdisplay) &&
isset($CFG->debug) &&
$CFG->debugdisplay &&
$CFG->debug === DEBUG_DEVELOPER
);
if ($debuginfo) {
if ($isinphpunittest || $hasdebugdeveloper) {
$message = "$message ($debuginfo)";
}
}
if (!$haserrorstring and defined('PHPUNIT_TEST') and PHPUNIT_TEST) {
if (!$haserrorstring and $isinphpunittest) {
// Append the contents of $a to $debuginfo so helpful information isn't lost.
// This emulates what {@link get_exception_info()} does. Unfortunately that
// function is not used by phpunit.
+1 -1
View File
@@ -33,4 +33,4 @@
Example context (json):
{}
}}
<span class="loading-icon">{{#pix}} i/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
<span class="loading-icon icon-no-margin">{{#pix}} i/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_view_contact","core_message/message_drawer_view_contacts","core_message/message_drawer_view_conversation","core_message/message_drawer_view_group_info","core_message/message_drawer_view_overview","core_message/message_drawer_view_search","core_message/message_drawer_view_settings","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_events"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={VIEW_CONTACT:'[data-region="view-contact"]',VIEW_CONTACTS:'[data-region="view-contacts"]',VIEW_CONVERSATION:'[data-region="view-conversation"]',VIEW_GROUP_INFO:'[data-region="view-group-info"]',VIEW_OVERVIEW:'[data-region="view-overview"]',VIEW_SEARCH:'[data-region="view-search"]',VIEW_SETTINGS:'[data-region="view-settings"]',ROUTES:"[data-route]",ROUTES_BACK:"[data-route-back]",HEADER_CONTAINER:'[data-region="header-container"]',BODY_CONTAINER:'[data-region="body-container"]',FOOTER_CONTAINER:'[data-region="footer-container"]'},o=function(a,b){var c=a.children(),d=c.filter(n.HEADER_CONTAINER).find(b),e=c.filter(n.BODY_CONTAINER).find(b),f=c.filter(n.FOOTER_CONTAINER).find(b),g=[d,e,f].filter(function(a){return a.length});return g},p=[[l.VIEW_CONTACT,n.VIEW_CONTACT,d.show,d.description],[l.VIEW_CONTACTS,n.VIEW_CONTACTS,e.show,e.description],[l.VIEW_CONVERSATION,n.VIEW_CONVERSATION,f.show,f.description],[l.VIEW_GROUP_INFO,n.VIEW_GROUP_INFO,g.show,g.description],[l.VIEW_OVERVIEW,n.VIEW_OVERVIEW,h.show,h.description],[l.VIEW_SEARCH,n.VIEW_SEARCH,i.show,i.description],[l.VIEW_SETTINGS,n.VIEW_SETTINGS,j.show,j.description]],q=function(a){p.forEach(function(b){k.add(b[0],o(a,b[1]),b[2],b[3])})},r=function(a){a.attr("data-shown")||(k.go(l.VIEW_OVERVIEW),a.attr("data-shown",!0)),a.removeClass("hidden"),a.attr("aria-expanded",!0),a.attr("aria-hidden",!1)},s=function(a){a.addClass("hidden"),a.attr("aria-expanded",!1),a.attr("aria-hidden",!0)},t=function(a){return!a.hasClass("hidden")},u=function(d){b.define(d,[b.events.activate]);var e=/^data-route-param-?(\d*)$/;d.on(b.events.activate,n.ROUTES,function(b,c){for(var d=a(b.target).closest(n.ROUTES),f=d.attr("data-route"),g=[],h=0;h<d[0].attributes.length;h++)g.push(d[0].attributes[h]);var i=g.filter(function(a){var b=a.nodeName,c=e.test(b);return c});i.sort(function(a,b){var c=e.exec(a.nodeName),d=e.exec(b.nodeName),f=c.length>1?c[1]:0,g=d.length>1?d[1]:0;return f<g?-1:g<f?1:0});var j=i.map(function(a){return a.nodeValue}),l=[f].concat(j);k.go.apply(null,l),c.originalEvent.preventDefault()}),d.on(b.events.activate,n.ROUTES_BACK,function(a,b){k.back(),b.originalEvent.preventDefault()}),c.subscribe(m.SHOW,function(){r(d)}),c.subscribe(m.HIDE,function(){s(d)}),c.subscribe(m.TOGGLE_VISIBILITY,function(){t(d)?s(d):r(d)}),c.subscribe(m.SHOW_CONVERSATION,function(a){r(d),k.go(l.VIEW_CONVERSATION,a)}),c.subscribe(m.CREATE_CONVERSATION_WITH_USER,function(a){r(d),k.go(l.VIEW_CONVERSATION,null,"create",a)}),c.subscribe(m.SHOW_SETTINGS,function(){r(d),k.go(l.VIEW_SETTINGS)}),c.subscribe(m.PREFERENCES_UPDATED,function(a){var b=a.filter(function(a){return"message_entertosend"==a.type}),c=b.length?b[0]:null;if(c){var e=d.find(n.FOOTER_CONTAINER).find(n.VIEW_CONVERSATION);e.attr("data-enter-to-send",c.value)}})},v=function(b){b=a(b),q(b),u(b)};return{init:v}});
+1
View File
@@ -0,0 +1 @@
define([],function(){return{CREATE_CONVERSATION_WITH_USER:"message-drawer-create-conversation-with-user",CONTACT_BLOCKED:"message-drawer-contact-blocked",CONTACT_UNBLOCKED:"message-drawer-contact-unblocked",CONTACT_ADDED:"message-drawer-contact-added",CONTACT_REMOVED:"message-drawer-contact-removed",CONTACT_REQUEST_ACCEPTED:"message-drawer-contact-request-accepted",CONTACT_REQUEST_DECLINED:"message-drawer-contact-request-declined",CONVERSATION_CREATED:"message-drawer-conversation-created",CONVERSATION_NEW_LAST_MESSAGE:"message-drawer-conversation-new-last-message",CONVERSATION_DELETED:"message-drawer-conversation-deleted",CONVERSATION_READ:"message-drawer-conversation-read",CONVERSATION_SET_FAVOURITE:"message-drawer-conversation-set-favourite",CONVERSATION_UNSET_FAVOURITE:"message-drawer-conversation-unset-favourite",PREFERENCES_UPDATED:"message-drawer-preferences-updated",ROUTE_CHANGED:"message-drawer-route-change",SHOW:"message-drawer-show",HIDE:"message-drawer-hide",TOGGLE_VISIBILITY:"message-drawer-toggle",SHOW_CONVERSATION:"message-drawer-show-conversation",SHOW_SETTINGS:"message-drawer-show-settings"}});
+1
View File
@@ -0,0 +1 @@
define(["core/pubsub","core_message/message_drawer_events"],function(a,b){var c=function(c){a.publish(b.CREATE_CONVERSATION_WITH_USER,c)},d=function(){a.publish(b.SHOW)},e=function(c){a.publish(b.SHOW_CONVERSATION,c)},f=function(){a.publish(b.SHOW_SETTINGS)};return{createConversationWithUser:c,show:d,showConversation:e,showSettings:f}});
@@ -0,0 +1 @@
define(["jquery","core/custom_interaction_events"],function(a,b){var c={ROOT:'[data-region="lazy-load-list"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',CONTENT_CONTAINER:'[data-region="content-container"]',EMPTY_MESSAGE:'[data-region="empty-message-container"]',PLACEHOLDER:'[data-region="placeholder-container"]'},d=function(a){a.attr("data-loading",!0)},e=function(a){a.attr("data-loading",!1)},f=function(a){return"true"===a.attr("data-loading")},g=function(a){return a.attr("data-user-id")},h=function(a){return a.find(c.CONTENT_CONTAINER)},i=function(a){return a.find(c.ROOT)},j=function(a){a.find(c.LOADING_ICON_CONTAINER).removeClass("hidden")},k=function(a){a.find(c.LOADING_ICON_CONTAINER).addClass("hidden")},l=function(a){a.find(c.EMPTY_MESSAGE).removeClass("hidden")},m=function(a){a.find(c.EMPTY_MESSAGE).addClass("hidden")},n=function(a){a.find(c.PLACEHOLDER).removeClass("hidden")},o=function(a){a.find(c.PLACEHOLDER).addClass("hidden")},p=function(a){h(a).removeClass("hidden")},q=function(a){h(a).addClass("hidden")},r=function(a){return"true"==a.attr("data-loaded-all")},s=function(a,b){a.attr("data-loaded-all",b)},t=function(a){return!r(a)&&!f(a)},u=function(a,b,c){var f=g(a);return d(a),b(a,f).then(function(b){if(b.length>0){var d=h(a);return c(d,b,f).then(function(){return b})}return b}).then(function(b){return e(a),a.attr("data-seen",!0),b.length||s(a,!0),b})["catch"](function(){e(a),a.attr("data-seen",!0)})},v=function(a,b,c){return h(a).empty(),n(a),q(a),u(a,b,c).then(function(b){o(a),b.length?p(a):l(a)})["catch"](function(){o(a),p(a)})},w=function(a,c,d){b.define(a,[b.events.scrollBottom]),a.on(b.events.scrollBottom,function(){t(a)&&(j(a),u(a,c,d).then(function(){return k(a)})["catch"](function(){return k(a)}))})},x=function(b,c,d){b=a(b),b.attr("data-init")||(w(b,c,d),v(b,c,d),b.attr("data-init",!0))};return{show:x,getContentContainer:h,getRoot:i,setLoadedAll:s,showEmptyMessage:l,hideEmptyMessage:m,showContent:p,hideContent:q}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/pubsub","core/str","core_message/message_drawer_events"],function(a,b,c,d){var e={},f=[],g={CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',ROUTES_BACK:"[data-route-back]"},h=function(a,b,c,d){e[a]={elements:b,onGo:c,getDescription:d}},i=function(c){var f,h=[].slice.call(arguments,1),i=a.Deferred().resolve().promise();if(Object.keys(e).forEach(function(a){var b=e[a],d=a===c;d&&(f=b),b.elements.forEach(function(a){a.removeClass("previous"),d?(a.removeClass("hidden"),a.attr("aria-hidden",!1)):(a.addClass("hidden"),a.attr("aria-hidden",!0))})}),f&&f.onGo){i=f.onGo.apply(void 0,f.elements.concat(h));for(var j=a(document.activeElement),k=!1,l=0;l<f.elements.length;l++){var m=f.elements[l];if(m.has(j).length){k=!0;break}}k||f.elements[0].find(g.CAN_RECEIVE_FOCUS).filter(":visible").first().focus()}var n={route:c,params:h,renderPromise:i};return b.publish(d.ROUTE_CHANGED,n),n},j=function(){var b=a(document.activeElement),d=i.apply(null,arguments),h=!1;f=f.reduce(function(a,b){return b.route===d.route&&(h=!0),h||a.push(b),a},[]);var j=f.length?f[f.length-1]:null;if(j){var k=e[j.route];k.elements.forEach(function(a){a.addClass("previous")}),j.focusElement=b,k.getDescription&&k.getDescription.apply(null,k.elements.concat(j.params)).then(function(a){return c.get_string("backto","core_message",a)}).then(function(a){return d.renderPromise.then(function(){e[d.route].elements.forEach(function(b){b.find(g.ROUTES_BACK).attr("aria-label",a)})})})["catch"](function(){})}return f.push(d),d},k=function(){if(f.length){f.pop();var a=f.pop();a&&(j.apply(void 0,[a.route].concat(a.params)),window.setTimeout(function(){a.focusElement.focus()},50))}};return{add:h,go:j,back:k}});
+1
View File
@@ -0,0 +1 @@
define([],function(){return{VIEW_CONTACT:"view-contact",VIEW_CONTACTS:"view-contacts",VIEW_CONVERSATION:"view-conversation",VIEW_GROUP_INFO:"view-group-info",VIEW_OVERVIEW:"view-overview",VIEW_SEARCH:"view-search",VIEW_SETTINGS:"view-settings"}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/str","core/templates"],function(a,b,c){var d={CONTENT_CONTAINER:'[data-region="content-container"]'},e={CONTENT:"core_message/message_drawer_view_contact_body_content"},f=function(a){return a.find(d.CONTENT_CONTAINER)},g=function(a,b){return c.render(e.CONTENT,b).then(function(b){return f(a).append(b),b})},h=function(b,c){return b=a(b),f(b).empty(),g(b,c)},i=function(a,c){return b.get_string("messagedrawerviewcontact","core_message",c.fullname)};return{show:h,description:i}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/pubsub","core/str","core_message/message_drawer_events","core_message/message_drawer_view_contacts_section_contacts","core_message/message_drawer_view_contacts_section_requests"],function(a,b,c,d,e,f){var g={ACTION_SHOW_CONTACTS_SECTION:'[data-action="show-contacts-section"]',ACTION_SHOW_REQUESTS_SECTION:'[data-action="show-requests-section"]',CONTACT_REQUEST_COUNT:'[data-region="contact-request-count"]',CONTACTS_SECTION_CONTAINER:'[data-section="contacts"]',REQUESTS_SECTION_CONTAINER:'[data-section="requests"]'},h=function(a){return a.find(g.CONTACTS_SECTION_CONTAINER)},i=function(a){return a.find(g.REQUESTS_SECTION_CONTAINER)},j=function(a){return a.hasClass("active")},k=function(a){return function(){var b=a.find(g.CONTACT_REQUEST_COUNT),c=parseInt(b.text(),10);c=isNaN(c)?0:c-1,c<=0?b.addClass("hidden"):b.text(c)}},l=function(a){var c=h(a),j=i(a),l=a.find(g.ACTION_SHOW_CONTACTS_SECTION),m=a.find(g.ACTION_SHOW_REQUESTS_SECTION);l.on("show.bs.tab",function(){e.show(c)}),m.on("show.bs.tab",function(){f.show(j)}),b.subscribe(d.CONTACT_REQUEST_ACCEPTED,k(a)),b.subscribe(d.CONTACT_REQUEST_DECLINED,k(a))},m=function(b,c){c=a(c),c.attr("data-contacts-init")||(l(c),c.attr("data-contacts-init",!0));var d=h(c),g=i(c);return j(d)?e.show(d):f.show(g),a.Deferred().resolve().promise()},n=function(){return c.get_string("messagedrawerviewcontacts","core_message")};return{show:m,description:n}});
@@ -0,0 +1 @@
define(["jquery","core/notification","core/pubsub","core/templates","core/custom_interaction_events","core_message/message_repository","core_message/message_drawer_events"],function(a,b,c,d,e,f,g){var h=100,i=0,j=0,k=!1,l=!1,m={BLOCK_ICON_CONTAINER:'[data-region="block-icon-container"]',CONTACTS:'[data-region="contacts-container"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',CONTENT_CONTAINER:'[data-region="contacts-content-container"]',EMPTY_MESSAGE:'[data-region="empty-message-container"]',PLACEHOLDER:'[data-region="placeholder-container"]'},n={CONTACTS_LIST:"core_message/message_drawer_contacts_list"},o=function(a){a.find(m.LOADING_ICON_CONTAINER).removeClass("hidden")},p=function(a){a.find(m.LOADING_ICON_CONTAINER).addClass("hidden")},q=function(a){return a.find(m.CONTENT_CONTAINER)},r=function(a){return a.find(m.CONTACTS)},s=function(a){q(a).addClass("hidden"),a.find(m.EMPTY_MESSAGE).removeClass("hidden")},t=function(a){a.find(m.PLACEHOLDER).addClass("hidden")},u=function(a){q(a).removeClass("hidden")},v=function(a,b){return a.find('[data-contact-user-id="'+b+'"]')},w=function(a){return a.attr("data-user-id")},x=function(a,b){var c=q(a);return d.render(n.CONTACTS_LIST,{contacts:b}).then(function(b){return t(a),c.append(b),u(a),b})},y=function(a){var b=w(a);return f.getContacts(b,h+1,j).then(function(a){return a.contacts}).then(function(a){return a.length>h?a.pop():k=!0,a}).then(function(b){return 0==j&&0==b.length&&(t(a),s(a)),i+=b.length,j+=h,b.length>0?x(a,b):b})},z=function(a,b){v(a,b).remove()},A=function(a,b){var c=v(a,b);c.length&&c.find(m.BLOCK_ICON_CONTAINER).removeClass("hidden")},B=function(a,b){var c=v(a,b);c.length&&c.find(m.BLOCK_ICON_CONTAINER).addClass("hidden")},C=function(a){c.subscribe(g.CONTACT_ADDED,function(){j=0,k=!1,q(a).empty(),y(a)}),c.subscribe(g.CONTACT_REMOVED,function(b){z(a,b)}),c.subscribe(g.CONTACT_BLOCKED,function(b){A(a,b)}),c.subscribe(g.CONTACT_UNBLOCKED,function(b){B(a,b)});var d=r(a);e.define(d,[e.events.scrollBottom,e.events.scrollLock]),d.on(e.events.scrollBottom,function(c,d){var e=i>1;k||!e||l||(l=!0,o(a),y(a).then(function(){p(a),l=!1})["catch"](function(c){p(a),l=!1,b.exception(c)})),d.originalEvent.preventDefault()})},D=function(b,c){c=a(c),j=0,c.attr("data-contacts-init")||(C(c),c.attr("data-contacts-init",!0)),k||y(c)};return{show:D}});
@@ -0,0 +1 @@
define(["jquery","core/notification","core/pubsub","core/templates","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e,f,g){var h=100,i=0,j={BLOCK_ICON_CONTAINER:'[data-region="block-icon-container"]',CONTACT:'[data-region="contact"]',CONTENT_CONTAINER:'[data-region="contacts-content-container"]'},k={CONTACTS_LIST:"core_message/message_drawer_contacts_list"},l=function(a,b){return a.find('[data-contact-user-id="'+b+'"]')},m=function(c,e){var f=e.map(function(b){return a.extend(b,{id:b.userid})});return d.render(k.CONTACTS_LIST,{contacts:f}).then(function(a){return c.append(a),a})["catch"](b.exception)},n=function(a,c){return e.getContacts(c,h+1,i).then(function(a){return a.contacts}).then(function(b){return b.length>h?b.pop():g.setLoadedAll(a,!0),b}).then(function(a){return i+=h,a})["catch"](b.exception)},o=function(a,b){l(a,b).remove()},p=function(a,b){var c=l(a,b);c.length&&c.find(j.BLOCK_ICON_CONTAINER).removeClass("hidden")},q=function(a,b){var c=l(a,b);c.length&&c.find(j.BLOCK_ICON_CONTAINER).addClass("hidden")},r=function(a){c.subscribe(f.CONTACT_ADDED,function(b){var c=g.getContentContainer(a);m(c,[b]),g.hideEmptyMessage(a),g.showContent(a)}),c.subscribe(f.CONTACT_REMOVED,function(b){o(a,b);var c=a.find(j.CONTACT);c.length||(g.hideContent(a),g.showEmptyMessage(a))}),c.subscribe(f.CONTACT_BLOCKED,function(b){p(a,b)}),c.subscribe(f.CONTACT_UNBLOCKED,function(b){q(a,b)})},s=function(a){a.attr("data-contacts-init")||(r(a),a.attr("data-contacts-init",!0)),g.show(a,n,m)};return{show:s}});
@@ -0,0 +1 @@
define(["jquery","core/notification","core/pubsub","core/templates","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e,f,g){var h={CONTACT_REQUEST:'[data-region="contact-request"]'},i={REQUESTS_LIST:"core_message/message_drawer_view_contacts_body_section_requests_list"},j=function(a,c){var e=c.map(function(a){return{id:a.id,profileimageurl:a.profileimageurl,fullname:a.fullname}});return d.render(i.REQUESTS_LIST,{requests:e}).then(function(b){return a.append(b),b})["catch"](b.exception)},k=function(a,c){return e.getContactRequests(c).then(function(b){return g.setLoadedAll(a,!0),b})["catch"](b.exception)},l=function(a){return function(b){a.find('[data-request-id="'+b.userid+'"]').remove();var c=a.find(h.CONTACT_REQUEST);c.length||(g.showEmptyMessage(a),g.hideContent(a))}},m=function(a){c.subscribe(f.CONTACT_REQUEST_ACCEPTED,l(a)),c.subscribe(f.CONTACT_REQUEST_DECLINED,l(a))},n=function(a){a.attr("data-contacts-init")||(m(a),a.attr("data-contacts-init",!0)),g.show(a,k,j)};return{show:n}});
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
define([],function(){var a={ACTION_ACCEPT_CONTACT_REQUEST:'[data-action="accept-contact-request"]',ACTION_CANCEL_CONFIRM:'[data-action="cancel-confirm"]',ACTION_CANCEL_EDIT_MODE:'[data-action="cancel-edit-mode"]',ACTION_CONFIRM_ADD_CONTACT:'[data-action="confirm-add-contact"]',ACTION_CONFIRM_BLOCK:'[data-action="confirm-block"]',ACTION_CONFIRM_DELETE_SELECTED_MESSAGES:'[data-action="confirm-delete-selected-messages"]',ACTION_CONFIRM_DELETE_CONVERSATION:'[data-action="confirm-delete-conversation"]',ACTION_CONFIRM_FAVOURITE:'[data-action="confirm-favourite"]',ACTION_CONFIRM_UNFAVOURITE:'[data-action="confirm-unfavourite"]',ACTION_CONFIRM_REMOVE_CONTACT:'[data-action="confirm-remove-contact"]',ACTION_CONFIRM_UNBLOCK:'[data-action="confirm-unblock"]',ACTION_DECLINE_CONTACT_REQUEST:'[data-action="decline-contact-request"]',ACTION_REQUEST_ADD_CONTACT:'[data-action="request-add-contact"]',ACTION_REQUEST_BLOCK:'[data-action="request-block"]',ACTION_REQUEST_DELETE_CONVERSATION:'[data-action="request-delete-conversation"]',ACTION_REQUEST_DELETE_SELECTED_MESSAGES:'[data-action="delete-selected-messages"]',ACTION_REQUEST_REMOVE_CONTACT:'[data-action="request-remove-contact"]',ACTION_REQUEST_UNBLOCK:'[data-action="request-unblock"]',ACTION_VIEW_CONTACT:'[data-action="view-contact"]',ACTION_VIEW_GROUP_INFO:'[data-action="view-group-info"]',CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',CONFIRM_DIALOGUE_BUTTON_TEXT:'[data-region="dialogue-button-text"]',CONFIRM_DIALOGUE_CANCEL_BUTTON:'[data-action="cancel-confirm"]',CONFIRM_DIALOGUE_CONTAINER:'[data-region="confirm-dialogue-container"]',CONFIRM_DIALOGUE_HEADER:'[data-region="dialogue-header"]',CONFIRM_DIALOGUE_TEXT:'[data-region="dialogue-text"]',CONTACT_REQUEST_SENT_MESSAGE_CONTAINER:'[data-region="contact-request-sent-message-container"]',CONTENT_PLACEHOLDER_CONTAINER:'[data-region="content-placeholder"]',CONTENT_CONTAINER:'[data-region="content-container"]',CONTENT_MESSAGES_CONTAINER:'[data-region="content-message-container"]',CONTENT_MESSAGES_FOOTER_CONTAINER:'[data-region="content-messages-footer-container"]',CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER:'[data-region="content-messages-footer-edit-mode-container"]',CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER:'[data-region="content-messages-footer-require-contact-container"]',CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER:'[data-region="content-messages-footer-require-unblock-container"]',CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER:'[data-region="content-messages-footer-unable-to-message"]',DAY_MESSAGES_CONTAINER:'[data-region="day-messages-container"]',FAVOURITE_ICON_CONTAINER:'[data-region="favourite-icon-container"]',FOOTER_CONTAINER:'[data-region="content-messages-footer-container"]',HEADER:'[data-region="header-content"]',HEADER_EDIT_MODE:'[data-region="header-edit-mode"]',HEADER_PLACEHOLDER_CONTAINER:'[data-region="header-placeholder"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',MESSAGE:'[data-region="message"]',MESSAGE_NOT_SELECTED:'[data-region="message"][aria-checked="false"]',MESSAGE_NOT_SELECTED_ICON:'[data-region="not-selected-icon"]',MESSAGE_SELECTED_ICON:'[data-region="selected-icon"]',MESSAGES:'[data-region="content-message-container"]',MESSAGES_CONTAINER:'[data-region="content-message-container"]',MESSAGES_SELECTED_COUNT:'[data-region="message-selected-court"]',MESSAGE_TEXT_AREA:'[data-region="send-message-txt"]',MORE_MESSAGES_LOADING_ICON_CONTAINER:'[data-region="more-messages-loading-icon-container"]',PLACEHOLDER_CONTAINER:'[data-region="placeholder-container"]',SEND_MESSAGE_BUTTON:'[data-action="send-message"]',SEND_MESSAGE_ICON_CONTAINER:'[data-region="send-icon-container"]',TEXT:'[data-region="text"]',TITLE:'[data-region="title"]'},b={HEADER_PRIVATE:"core_message/message_drawer_view_conversation_header_content_type_private",HEADER_PRIVATE_NO_CONTROLS:"core_message/message_drawer_view_conversation_header_content_type_private_no_controls",HEADER_PUBLIC:"core_message/message_drawer_view_conversation_header_content_type_public",DAY:"core_message/message_drawer_view_conversation_body_day",MESSAGE:"core_message/message_drawer_view_conversation_body_message",MESSAGES:"core_message/message_drawer_view_conversation_body_messages"},c={PRIVATE:1,PUBLIC:2};return{SELECTORS:a,TEMPLATES:b,CONVERSATION_TYPES:c,NEWEST_MESSAGES_FIRST:!0,LOAD_MESSAGE_LIMIT:100,INITIAL_NEW_MESSAGE_POLL_TIMEOUT:1e3}});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
define(["jquery","core/str","core/templates","core_message/message_repository","core_message/message_drawer_lazy_load_list"],function(a,b,c,d,e){var f=50,g={CONTENT_CONTAINER:'[data-region="group-info-content-container"]',MEMBERS_LIST:'[data-region="members-list"]'},h={CONTENT:"core_message/message_drawer_view_group_info_body_content",MEMBERS_LIST:"core_message/message_drawer_view_group_info_participants_list"},i=function(a){return a.find(g.CONTENT_CONTAINER)},j=function(a,b,d){var e=b.totalMemberCount>50?50:b.totalMemberCount,f=Array.apply(null,Array(e)).map(function(){return!0}),g={name:b.name,subname:b.subname,imageurl:b.imageUrl,placeholders:f,loggedinuser:{id:d}};return c.render(h.CONTENT,g).then(function(b){return i(a).append(b),b})},k=function(a,b,c){return function(f,g){return d.getConversationMembers(a.id,g,b+1,c).then(function(a){return a.length>b?a=a.slice(0,-1):e.setLoadedAll(f,!0),c+=b,a})}},l=function(a,b){return c.render(h.MEMBERS_LIST,{contacts:b}).then(function(b){return a.append(b),b})},m=function(b,c,d){return b=a(b),i(b).empty(),j(b,c,d).then(function(){var a=e.getRoot(b);e.show(a,k(c,f,0),l)})},n=function(a,c){return b.get_string("messagedrawerviewgroupinfo","core_message",c.name)};return{show:m,description:n}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/key_codes","core/pubsub","core/str","core_message/message_drawer_view_overview_section_favourites","core_message/message_drawer_view_overview_section_group_messages","core_message/message_drawer_view_overview_section_messages","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_events"],function(a,b,c,d,e,f,g,h,i,j){var k={CONTACT_REQUEST_COUNT:'[data-region="contact-request-count"]',FAVOURITES:'[data-region="view-overview-favourites"]',GROUP_MESSAGES:'[data-region="view-overview-group-messages"]',MESSAGES:'[data-region="view-overview-messages"]',SEARCH_INPUT:'[data-region="view-overview-search-input"]'},l=function(a){return a.find(k.SEARCH_INPUT)},m=function(a){return function(){var b=a.find(k.CONTACT_REQUEST_COUNT),c=parseInt(b.text(),10);c=isNaN(c)?0:c-1,c<=0?b.addClass("hidden"):b.text(c)}},n=function(a){var d=l(a),e=[b.tab,b.shift,b.ctrl,b.alt];d.on("click",function(){h.go(i.VIEW_SEARCH)}),d.on("keydown",function(a){e.indexOf(a.keyCode)<0&&"Meta"!=a.key&&h.go(i.VIEW_SEARCH)}),c.subscribe(j.CONTACT_REQUEST_ACCEPTED,m(a)),c.subscribe(j.CONTACT_REQUEST_DECLINED,m(a))},o=function(b,c){return b.attr("data-init")||(n(b),b.attr("data-init",!0)),l(b).val(""),a.when(e.show(c.find(k.FAVOURITES)),f.show(c.find(k.GROUP_MESSAGES)),g.show(c.find(k.MESSAGES)))},p=function(){return d.get_string("messagedrawerviewoverview","core_message")};return{show:o,description:p}});
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=null,d=!0,e=function(e){b.show(a(e),c,d)};return{show:e}});
@@ -0,0 +1 @@
define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=2,d=!1,e=function(e){e=a(e),b.show(a(e),c,d)};return{show:e}});
@@ -0,0 +1 @@
define(["jquery","core_message/message_drawer_view_overview_section"],function(a,b){var c=1,d=!1,e=function(e){b.show(a(e),c,d)};return{show:e}});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/notification","core/str","core/pubsub","core_message/message_repository","core/custom_interaction_events","core_message/message_drawer_events"],function(a,b,c,d,e,f,g){var h={SETTINGS:'[data-region="settings"]',PREFERENCE_CONTROL:'[data-region="preference-control"]',PRIVACY_PREFERENCE:'[data-preference="blocknoncontacts"] input[type="radio"]',EMAIL_ENABLED_PREFERENCE:'[data-preference="emailnotifications"] input[type="checkbox"]',ENTER_TO_SEND_PREFERENCE:'[data-preference="entertosend"] input[type="checkbox"]'},i={message_provider_moodle_instantmessage_loggedoff:{type:"emailnotifications",enabled:"email",disabled:"none"},message_provider_moodle_instantmessage_loggedin:{type:"emailnotifications",enabled:"email",disabled:"none"}},j=function(c,j){var k=c.find(h.SETTINGS);f.define(k,[f.events.activate]),k.on(f.events.activate,h.EMAIL_ENABLED_PREFERENCE,function(c){var f=a(c.target),k=f.closest(h.PREFERENCE_CONTROL),l=k.attr("data-preference"),m=f.prop("checked"),n=Object.keys(i).reduce(function(a,b){var c=i[b];return c.type===l&&a.push({type:b,value:m?c.enabled:c.disabled}),a},[]);e.savePreferences(j,n).then(function(){d.publish(g.PREFERENCES_UPDATED,n)})["catch"](b.exception)}),k.on(f.events.activate,h.PRIVACY_PREFERENCE,function(c){var f=a(c.target).val(),h=[{type:"message_blocknoncontacts",value:f}];e.savePreferences(j,h).then(function(){d.publish(g.PREFERENCES_UPDATED,h)})["catch"](b.exception)}),k.on(f.events.activate,h.ENTER_TO_SEND_PREFERENCE,function(c){var f=a(c.target).prop("checked"),h=[{type:"message_entertosend",value:f}];e.savePreferences(j,h).then(function(){d.publish(g.PREFERENCES_UPDATED,h)})["catch"](b.exception)})},k=function(b,c,d){return c.attr("data-init")||(j(c,d),c.attr("data-init",!0)),a.Deferred().resolve().promise()},l=function(){return c.get_string("messagedrawerviewsettings","core_message")};return{show:k,description:l}});
+1
View File
@@ -0,0 +1 @@
define(["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_events"],function(a,b,c,d){var e={COUNT_CONTAINER:'[data-region="count-container"]'},f=function(){c.publish(d.TOGGLE_VISIBILITY)},g=function(a){return function(){var b=a.find(e.COUNT_CONTAINER),c=parseInt(b.text(),10);isNaN(c)?b.addClass("hidden"):!c||c<2?b.addClass("hidden"):(c-=1,b.text(c))}},h=function(a){b.define(a,[b.events.activate]),a.on(b.events.activate,function(a,b){f(),b.originalEvent.preventDefault()}),c.subscribe(d.CONVERSATION_READ,g(a)),c.subscribe(d.CONTACT_REQUEST_ACCEPTED,g(a)),c.subscribe(d.CONTACT_REQUEST_DECLINED,g(a))},i=function(b){b=a(b),h(b)};return{init:i}});
File diff suppressed because one or more lines are too long
+260
View File
@@ -0,0 +1,260 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the message drawer.
*
* @module core_message/message_drawer
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events',
'core/pubsub',
'core_message/message_drawer_view_contact',
'core_message/message_drawer_view_contacts',
'core_message/message_drawer_view_conversation',
'core_message/message_drawer_view_group_info',
'core_message/message_drawer_view_overview',
'core_message/message_drawer_view_search',
'core_message/message_drawer_view_settings',
'core_message/message_drawer_router',
'core_message/message_drawer_routes',
'core_message/message_drawer_events'
],
function(
$,
CustomEvents,
PubSub,
ViewContact,
ViewContacts,
ViewConversation,
ViewGroupInfo,
ViewOverview,
ViewSearch,
ViewSettings,
Router,
Routes,
Events
) {
var SELECTORS = {
VIEW_CONTACT: '[data-region="view-contact"]',
VIEW_CONTACTS: '[data-region="view-contacts"]',
VIEW_CONVERSATION: '[data-region="view-conversation"]',
VIEW_GROUP_INFO: '[data-region="view-group-info"]',
VIEW_OVERVIEW: '[data-region="view-overview"]',
VIEW_SEARCH: '[data-region="view-search"]',
VIEW_SETTINGS: '[data-region="view-settings"]',
ROUTES: '[data-route]',
ROUTES_BACK: '[data-route-back]',
HEADER_CONTAINER: '[data-region="header-container"]',
BODY_CONTAINER: '[data-region="body-container"]',
FOOTER_CONTAINER: '[data-region="footer-container"]',
};
/**
* Get elements for route.
*
* @param {Object} root The message drawer container.
* @param {string} selector The route container.
*
* @return {array} elements Found route container objects.
*/
var getElementsForRoute = function(root, selector) {
var candidates = root.children();
var header = candidates.filter(SELECTORS.HEADER_CONTAINER).find(selector);
var body = candidates.filter(SELECTORS.BODY_CONTAINER).find(selector);
var footer = candidates.filter(SELECTORS.FOOTER_CONTAINER).find(selector);
var elements = [header, body, footer].filter(function(element) {
return element.length;
});
return elements;
};
var routes = [
[Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description],
[Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description],
[Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description],
[Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description],
[Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description],
[Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description],
[Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description],
];
/**
* Create routes.
*
* @param {Object} root The message drawer container.
*/
var createRoutes = function(root) {
routes.forEach(function(route) {
Router.add(route[0], getElementsForRoute(root, route[1]), route[2], route[3]);
});
};
/**
* Show the message drawer.
*
* @param {Object} root The message drawer container.
*/
var show = function(root) {
if (!root.attr('data-shown')) {
Router.go(Routes.VIEW_OVERVIEW);
root.attr('data-shown', true);
}
root.removeClass('hidden');
root.attr('aria-expanded', true);
root.attr('aria-hidden', false);
};
/**
* Hide the message drawer.
*
* @param {Object} root The message drawer container.
*/
var hide = function(root) {
root.addClass('hidden');
root.attr('aria-expanded', false);
root.attr('aria-hidden', true);
};
/**
* Check if the drawer is visible.
*
* @param {Object} root The message drawer container.
* @return {bool}
*/
var isVisible = function(root) {
return !root.hasClass('hidden');
};
/**
* Listen to and handle events for routing, showing and hiding the message drawer.
*
* @param {Object} root The message drawer container.
*/
var registerEventListeners = function(root) {
CustomEvents.define(root, [CustomEvents.events.activate]);
var paramRegex = /^data-route-param-?(\d*)$/;
root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {
var element = $(e.target).closest(SELECTORS.ROUTES);
var route = element.attr('data-route');
var attributes = [];
for (var i = 0; i < element[0].attributes.length; i++) {
attributes.push(element[0].attributes[i]);
}
var paramAttributes = attributes.filter(function(attribute) {
var name = attribute.nodeName;
var match = paramRegex.test(name);
return match;
});
paramAttributes.sort(function(a, b) {
var aParts = paramRegex.exec(a.nodeName);
var bParts = paramRegex.exec(b.nodeName);
var aIndex = aParts.length > 1 ? aParts[1] : 0;
var bIndex = bParts.length > 1 ? bParts[1] : 0;
if (aIndex < bIndex) {
return -1;
} else if (bIndex < aIndex) {
return 1;
} else {
return 0;
}
});
var params = paramAttributes.map(function(attribute) {
return attribute.nodeValue;
});
var routeParams = [route].concat(params);
Router.go.apply(null, routeParams);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) {
Router.back();
data.originalEvent.preventDefault();
});
PubSub.subscribe(Events.SHOW, function() {
show(root);
});
PubSub.subscribe(Events.HIDE, function() {
hide(root);
});
PubSub.subscribe(Events.TOGGLE_VISIBILITY, function() {
if (isVisible(root)) {
hide(root);
} else {
show(root);
}
});
PubSub.subscribe(Events.SHOW_CONVERSATION, function(conversationId) {
show(root);
Router.go(Routes.VIEW_CONVERSATION, conversationId);
});
PubSub.subscribe(Events.CREATE_CONVERSATION_WITH_USER, function(userId) {
show(root);
Router.go(Routes.VIEW_CONVERSATION, null, 'create', userId);
});
PubSub.subscribe(Events.SHOW_SETTINGS, function() {
show(root);
Router.go(Routes.VIEW_SETTINGS);
});
PubSub.subscribe(Events.PREFERENCES_UPDATED, function(preferences) {
var filteredPreferences = preferences.filter(function(preference) {
return preference.type == 'message_entertosend';
});
var enterToSendPreference = filteredPreferences.length ? filteredPreferences[0] : null;
if (enterToSendPreference) {
var viewConversationFooter = root.find(SELECTORS.FOOTER_CONTAINER).find(SELECTORS.VIEW_CONVERSATION);
viewConversationFooter.attr('data-enter-to-send', enterToSendPreference.value);
}
});
};
/**
* Initialise the message drawer.
*
* @param {Object} root The message drawer container.
*/
var init = function(root) {
root = $(root);
createRoutes(root);
registerEventListeners(root);
};
return {
init: init,
};
});
+47
View File
@@ -0,0 +1,47 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Events for the message drawer.
*
* @module core_message/message_drawer_events
* @package message
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
CREATE_CONVERSATION_WITH_USER: 'message-drawer-create-conversation-with-user',
CONTACT_BLOCKED: 'message-drawer-contact-blocked',
CONTACT_UNBLOCKED: 'message-drawer-contact-unblocked',
CONTACT_ADDED: 'message-drawer-contact-added',
CONTACT_REMOVED: 'message-drawer-contact-removed',
CONTACT_REQUEST_ACCEPTED: 'message-drawer-contact-request-accepted',
CONTACT_REQUEST_DECLINED: 'message-drawer-contact-request-declined',
CONVERSATION_CREATED: 'message-drawer-conversation-created',
CONVERSATION_NEW_LAST_MESSAGE: 'message-drawer-conversation-new-last-message',
CONVERSATION_DELETED: 'message-drawer-conversation-deleted',
CONVERSATION_READ: 'message-drawer-conversation-read',
CONVERSATION_SET_FAVOURITE: 'message-drawer-conversation-set-favourite',
CONVERSATION_UNSET_FAVOURITE: 'message-drawer-conversation-unset-favourite',
PREFERENCES_UPDATED: 'message-drawer-preferences-updated',
ROUTE_CHANGED: 'message-drawer-route-change',
SHOW: 'message-drawer-show',
HIDE: 'message-drawer-hide',
TOGGLE_VISIBILITY: 'message-drawer-toggle',
SHOW_CONVERSATION: 'message-drawer-show-conversation',
SHOW_SETTINGS: 'message-drawer-show-settings',
};
});
+72
View File
@@ -0,0 +1,72 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Provides some helper functions to trigger actions in the message drawer.
*
* @module core_message/message_drawer_helper
* @package message
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'core/pubsub',
'core_message/message_drawer_events'
],
function(
PubSub,
MessageDrawerEvents
) {
/**
* Trigger an event to create a new conversation in the message drawer.
*
* @param {Number} userId The user id to start a conversation.
*/
var createConversationWithUser = function(userId) {
PubSub.publish(MessageDrawerEvents.CREATE_CONVERSATION_WITH_USER, userId);
};
/**
* Trigger an event to show the message drawer.
*/
var show = function() {
PubSub.publish(MessageDrawerEvents.SHOW);
};
/**
* Trigger an event to show the given conversation.
*
* @param {int} conversationId Id for the conversation to show.
*/
var showConversation = function(conversationId) {
PubSub.publish(MessageDrawerEvents.SHOW_CONVERSATION, conversationId);
};
/**
* Trigger an event to show messaging settings.
*/
var showSettings = function() {
PubSub.publish(MessageDrawerEvents.SHOW_SETTINGS);
};
return {
createConversationWithUser: createConversationWithUser,
show: show,
showConversation: showConversation,
showSettings: showSettings
};
});
@@ -0,0 +1,327 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Lazy loaded list of items.
*
* @module core_message/message_drawer_lazy_load_list
* @package message
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events'
],
function(
$,
CustomEvents
) {
var SELECTORS = {
ROOT: '[data-region="lazy-load-list"]',
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
CONTENT_CONTAINER: '[data-region="content-container"]',
EMPTY_MESSAGE: '[data-region="empty-message-container"]',
PLACEHOLDER: '[data-region="placeholder-container"]'
};
/**
* Flag element as loading.
*
* @param {Object} root The section container element.
*/
var startLoading = function(root) {
root.attr('data-loading', true);
};
/**
* Flag element as not loading.
*
* @param {Object} root The section container element.
*/
var stopLoading = function(root) {
root.attr('data-loading', false);
};
/**
* Check if the element is loading.
*
* @param {Object} root The section container element.
* @return {Bool}
*/
var isLoading = function(root) {
return root.attr('data-loading') === 'true';
};
/**
* Get user id
*
* @param {Object} root The section container element.
* @return {Number} Logged in user id.
*/
var getUserId = function(root) {
return root.attr('data-user-id');
};
/**
* Get the section content container element.
*
* @param {Object} root The section container element.
* @return {Object} The section content container element.
*/
var getContentContainer = function(root) {
return root.find(SELECTORS.CONTENT_CONTAINER);
};
/**
* Get the root element.
*
* @param {Object} containerElement The container element to search in.
* @return {Object} The list root element.
*/
var getRoot = function(containerElement) {
return containerElement.find(SELECTORS.ROOT);
};
/**
* Show the loading icon.
*
* @param {Object} root The section container element.
*/
var showLoadingIcon = function(root) {
root.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
};
/**
* Hide the loading icon.
*
* @param {Object} root The section container element.
*/
var hideLoadingIcon = function(root) {
root.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
};
/**
* Show the empty message.
*
* @param {Object} root The section container element.
*/
var showEmptyMessage = function(root) {
root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
};
/**
* Hide the empty message.
*
* @param {Object} root The section container element.
*/
var hideEmptyMessage = function(root) {
root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
};
/**
* Show the placeholder element.
*
* @param {Object} root The section container element.
*/
var showPlaceholder = function(root) {
root.find(SELECTORS.PLACEHOLDER).removeClass('hidden');
};
/**
* Hide the placeholder element.
*
* @param {Object} root The section container element.
*/
var hidePlaceholder = function(root) {
root.find(SELECTORS.PLACEHOLDER).addClass('hidden');
};
/**
* Show the section content container.
*
* @param {Object} root The section container element.
*/
var showContent = function(root) {
getContentContainer(root).removeClass('hidden');
};
/**
* Hide the section content container.
*
* @param {Object} root The section container element.
*/
var hideContent = function(root) {
getContentContainer(root).addClass('hidden');
};
/**
* If the section has loaded all content.
*
* @param {Object} root The section container element.
* @return {Bool}
*/
var hasLoadedAll = function(root) {
return root.attr('data-loaded-all') == 'true';
};
/**
* If the section has loaded all content.
*
* @param {Object} root The section container element.
* @param {Bool} value If all items have been loaded.
*/
var setLoadedAll = function(root, value) {
root.attr('data-loaded-all', value);
};
/**
* If the section can load more items.
*
* @param {Object} root The section container element.
* @return {Bool}
*/
var canLoadMore = function(root) {
return !hasLoadedAll(root) && !isLoading(root);
};
/**
* Load all items in this container from callback and render them.
*
* @param {Object} root The section container element.
* @param {Function} loadCallback The callback to load items.
* @param {Function} renderCallback The callback to render the results.
* @return {Object} jQuery promise
*/
var loadAndRender = function(root, loadCallback, renderCallback) {
var userId = getUserId(root);
startLoading(root);
return loadCallback(root, userId)
.then(function(items) {
if (items.length > 0) {
var contentContainer = getContentContainer(root);
return renderCallback(contentContainer, items, userId)
.then(function() {
return items;
});
} else {
return items;
}
})
.then(function(items) {
stopLoading(root);
root.attr('data-seen', true);
if (!items.length) {
setLoadedAll(root, true);
}
return items;
})
.catch(function() {
stopLoading(root);
root.attr('data-seen', true);
return;
});
};
/**
* First load of this section.
*
* @param {Object} root The section container element.
* @param {Function} loadCallback The callback to load items.
* @param {Function} renderCallback The callback to render the results.
* @return {Object} promise
*/
var initialLoadAndRender = function(root, loadCallback, renderCallback) {
getContentContainer(root).empty();
showPlaceholder(root);
hideContent(root);
return loadAndRender(root, loadCallback, renderCallback)
.then(function(items) {
hidePlaceholder(root);
if (!items.length) {
showEmptyMessage(root);
} else {
showContent(root);
}
return;
})
.catch(function() {
hidePlaceholder(root);
showContent(root);
return;
});
};
/**
* Listen to, and handle events in this section.
*
* @param {Object} root The section container element.
* @param {Function} loadCallback The callback to load items.
* @param {Function} renderCallback The callback to render the results.
*/
var registerEventListeners = function(root, loadCallback, renderCallback) {
CustomEvents.define(root, [
CustomEvents.events.scrollBottom
]);
root.on(CustomEvents.events.scrollBottom, function() {
if (canLoadMore(root)) {
showLoadingIcon(root);
loadAndRender(root, loadCallback, renderCallback)
.then(function() {
return hideLoadingIcon(root);
})
.catch(function() {
return hideLoadingIcon(root);
});
}
});
};
/**
* Setup the section.
*
* @param {Object} root The section container element.
* @param {Function} loadCallback The callback to load items.
* @param {Function} renderCallback The callback to render the results.
*/
var show = function(root, loadCallback, renderCallback) {
root = $(root);
if (!root.attr('data-init')) {
registerEventListeners(root, loadCallback, renderCallback);
initialLoadAndRender(root, loadCallback, renderCallback);
root.attr('data-init', true);
}
};
return {
show: show,
getContentContainer: getContentContainer,
getRoot: getRoot,
setLoadedAll: setLoadedAll,
showEmptyMessage: showEmptyMessage,
hideEmptyMessage: hideEmptyMessage,
showContent: showContent,
hideContent: hideContent
};
});
+227
View File
@@ -0,0 +1,227 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* A simple router for the message drawer that allows navigating between
* the "pages" in the drawer.
*
* This module will maintain a linear history of the unique pages access
* to allow navigating back.
*
* @module core_message/message_drawer_router
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/pubsub',
'core/str',
'core_message/message_drawer_events'
],
function(
$,
PubSub,
Str,
MessageDrawerEvents
) {
/* @var {object} routes Message drawer route elements and callbacks. */
var routes = {};
/* @var {array} history Store for route objects history. */
var history = [];
var SELECTORS = {
CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
ROUTES_BACK: '[data-route-back]'
};
/**
* Add a route.
*
* @param {string} route Route config name.
* @param {array} elements Route container objects.
* @param {callback} onGo Route initialization function.
* @param {callback} getDescription Route initialization function.
*/
var add = function(route, elements, onGo, getDescription) {
routes[route] = {
elements: elements,
onGo: onGo,
getDescription: getDescription
};
};
/**
* Go to a defined route and run the route callbacks.
*
* @param {string} newRoute Route config name.
* @return {object} record Current route record with route config name and parameters.
*/
var changeRoute = function(newRoute) {
var newConfig;
// Get the rest of the arguments, if any.
var args = [].slice.call(arguments, 1);
var renderPromise = $.Deferred().resolve().promise();
Object.keys(routes).forEach(function(route) {
var config = routes[route];
var isMatch = route === newRoute;
if (isMatch) {
newConfig = config;
}
config.elements.forEach(function(element) {
element.removeClass('previous');
if (isMatch) {
element.removeClass('hidden');
element.attr('aria-hidden', false);
} else {
element.addClass('hidden');
element.attr('aria-hidden', true);
}
});
});
if (newConfig) {
if (newConfig.onGo) {
renderPromise = newConfig.onGo.apply(undefined, newConfig.elements.concat(args));
var currentFocusElement = $(document.activeElement);
var hasFocus = false;
for (var i = 0; i < newConfig.elements.length; i++) {
var element = newConfig.elements[i];
if (element.has(currentFocusElement).length) {
hasFocus = true;
break;
}
}
if (!hasFocus) {
// This page doesn't have focus yet so focus the first focusable
// element in the new view.
newConfig.elements[0].find(SELECTORS.CAN_RECEIVE_FOCUS).filter(':visible').first().focus();
}
}
}
var record = {
route: newRoute,
params: args,
renderPromise: renderPromise
};
PubSub.publish(MessageDrawerEvents.ROUTE_CHANGED, record);
return record;
};
/**
* Go to a defined route and store the route history.
*
* @param {string} newRoute Route config name.
* @return {object} record Current route record with route config name and parameters.
*/
var go = function() {
var currentFocusElement = $(document.activeElement);
var record = changeRoute.apply(null, arguments);
var inHistory = false;
// History stores a unique list of routes. Check to see if the new route
// is already in the history, if it is then forget all history after it.
// This ensures there are no duplicate routes in history and that it represents
// a linear path of routes (it never stores something like [foo, bar, foo])/
history = history.reduce(function(carry, previous) {
if (previous.route === record.route) {
inHistory = true;
}
if (!inHistory) {
carry.push(previous);
}
return carry;
}, []);
var previousRecord = history.length ? history[history.length - 1] : null;
if (previousRecord) {
var prevConfig = routes[previousRecord.route];
prevConfig.elements.forEach(function(element) {
element.addClass('previous');
});
previousRecord.focusElement = currentFocusElement;
if (prevConfig.getDescription) {
// If the route has a description then set it on the back button for
// the new page we're displaying.
prevConfig.getDescription.apply(null, prevConfig.elements.concat(previousRecord.params))
.then(function(description) {
return Str.get_string('backto', 'core_message', description);
})
.then(function(label) {
// Wait for the new page to finish rendering so that we know
// that the back button is visible.
return record.renderPromise.then(function() {
// Find the elements for the new route we displayed.
routes[record.route].elements.forEach(function(element) {
// Update the aria label for the back button.
element.find(SELECTORS.ROUTES_BACK).attr('aria-label', label);
});
return;
});
})
.catch(function() {
// Silently ignore.
});
}
}
history.push(record);
return record;
};
/**
* Go back to the previous route record stored in history.
*/
var back = function() {
if (history.length) {
// Remove the current route.
history.pop();
var previous = history.pop();
if (previous) {
// If we have a previous route then show it.
go.apply(undefined, [previous.route].concat(previous.params));
// Delay the focus 50 milliseconds otherwise it doesn't correctly
// focus the element for some reason...
window.setTimeout(function() {
previous.focusElement.focus();
}, 50);
}
}
};
return {
add: add,
go: go,
back: back
};
});
+33
View File
@@ -0,0 +1,33 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Available routes for the message drawer.
*
* @module core_message/message_drawer_routes
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
VIEW_CONTACT: 'view-contact',
VIEW_CONTACTS: 'view-contacts',
VIEW_CONVERSATION: 'view-conversation',
VIEW_GROUP_INFO: 'view-group-info',
VIEW_OVERVIEW: 'view-overview',
VIEW_SEARCH: 'view-search',
VIEW_SETTINGS: 'view-settings'
};
});
@@ -0,0 +1,97 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the contact page in the message drawer.
*
* @module core_message/message_drawer_view_contact
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/str',
'core/templates'
],
function(
$,
Str,
Templates
) {
var SELECTORS = {
CONTENT_CONTAINER: '[data-region="content-container"]'
};
var TEMPLATES = {
CONTENT: 'core_message/message_drawer_view_contact_body_content'
};
/**
* Get the content container of the contact view container.
*
* @param {Object} root Contact container element.
* @returns {Object} jQuery object
*/
var getContentContainer = function(root) {
return root.find(SELECTORS.CONTENT_CONTAINER);
};
/**
* Render the contact profile in the content container.
*
* @param {Object} root Contact container element.
* @param {Object} profile Contact profile details.
* @returns {Object} jQuery promise
*/
var render = function(root, profile) {
return Templates.render(TEMPLATES.CONTENT, profile)
.then(function(html) {
getContentContainer(root).append(html);
return html;
});
};
/**
* Setup the contact page.
*
* @param {Object} root Contact container element.
* @param {Object} contact The contact object.
* @returns {Object} jQuery promise
*/
var show = function(root, contact) {
root = $(root);
getContentContainer(root).empty();
return render(root, contact);
};
/**
* String describing this page used for aria-labels.
*
* @param {Object} root Contact container element.
* @param {Object} contact The contact object.
* @return {Object} jQuery promise
*/
var description = function(root, contact) {
return Str.get_string('messagedrawerviewcontact', 'core_message', contact.fullname);
};
return {
show: show,
description: description
};
});
@@ -0,0 +1,163 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the contacts page of the message drawer.
*
* @module core_message/message_drawer_view_contacts
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/pubsub',
'core/str',
'core_message/message_drawer_events',
'core_message/message_drawer_view_contacts_section_contacts',
'core_message/message_drawer_view_contacts_section_requests'
],
function(
$,
PubSub,
Str,
MessageDrawerEvents,
ContactsSection,
RequestsSection
) {
var SELECTORS = {
ACTION_SHOW_CONTACTS_SECTION: '[data-action="show-contacts-section"]',
ACTION_SHOW_REQUESTS_SECTION: '[data-action="show-requests-section"]',
CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]',
CONTACTS_SECTION_CONTAINER: '[data-section="contacts"]',
REQUESTS_SECTION_CONTAINER: '[data-section="requests"]',
};
/**
* Get the container element for the contacts section.
*
* @param {Object} body Contacts page body element.
* @return {Object}
*/
var getContactsSectionContainer = function(body) {
return body.find(SELECTORS.CONTACTS_SECTION_CONTAINER);
};
/**
* Get the container element for the requests section.
*
* @param {Object} body Contacts page body element.
* @return {Object}
*/
var getRequestsSectionContainer = function(body) {
return body.find(SELECTORS.REQUESTS_SECTION_CONTAINER);
};
/**
* Check if the given section is visible.
*
* @param {Object} sectionRoot The root element for the section
* @return {Bool}
*/
var isSectionVisible = function(sectionRoot) {
return sectionRoot.hasClass('active');
};
/**
* Decrement the contact request count. If the count is zero or below then
* hide the count.
*
* @param {Object} body Conversation body container element.
* @return {Function} A function to handle decrementing the count.
*/
var decrementContactRequestCount = function(body) {
return function() {
var countContainer = body.find(SELECTORS.CONTACT_REQUEST_COUNT);
var count = parseInt(countContainer.text(), 10);
count = isNaN(count) ? 0 : count - 1;
if (count <= 0) {
countContainer.addClass('hidden');
} else {
countContainer.text(count);
}
};
};
/**
* Listen to and handle events for contacts.
*
* @param {Object} body Contacts body container element.
*/
var registerEventListeners = function(body) {
var contactsSection = getContactsSectionContainer(body);
var requestsSection = getRequestsSectionContainer(body);
var showContactsAction = body.find(SELECTORS.ACTION_SHOW_CONTACTS_SECTION);
var showRequestsAction = body.find(SELECTORS.ACTION_SHOW_REQUESTS_SECTION);
showContactsAction.on('show.bs.tab', function() {
ContactsSection.show(contactsSection);
});
showRequestsAction.on('show.bs.tab', function() {
RequestsSection.show(requestsSection);
});
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(body));
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(body));
};
/**
* Setup the contact page.
*
* @param {Object} header Contacts header container element.
* @param {Object} body Contacts body container element.
* @return {Object} jQuery promise
*/
var show = function(header, body) {
body = $(body);
if (!body.attr('data-contacts-init')) {
registerEventListeners(body);
body.attr('data-contacts-init', true);
}
var contactsSection = getContactsSectionContainer(body);
var requestsSection = getRequestsSectionContainer(body);
if (isSectionVisible(contactsSection)) {
ContactsSection.show(contactsSection);
} else {
RequestsSection.show(requestsSection);
}
return $.Deferred().resolve().promise();
};
/**
* String describing this page used for aria-labels.
*
* @return {Object} jQuery promise
*/
var description = function() {
return Str.get_string('messagedrawerviewcontacts', 'core_message');
};
return {
show: show,
description: description
};
});
@@ -0,0 +1,317 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls a section on the contacts page of the message drawer.
*
* @module core_message/message_drawer_view_contacts_section
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/pubsub',
'core/templates',
'core/custom_interaction_events',
'core_message/message_repository',
'core_message/message_drawer_events'
],
function(
$,
Notification,
PubSub,
Templates,
CustomEvents,
MessageRepository,
Events
) {
var LOAD_CONTACTS_LIMIT = 100;
var numContacts = 0;
var contactsOffset = 0;
var loadedAllContacts = false;
var waitForScrollLoad = false;
var SELECTORS = {
BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]',
CONTACTS: '[data-region="contacts-container"]',
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
CONTENT_CONTAINER: '[data-region="contacts-content-container"]',
EMPTY_MESSAGE: '[data-region="empty-message-container"]',
PLACEHOLDER: '[data-region="placeholder-container"]'
};
var TEMPLATES = {
CONTACTS_LIST: 'core_message/message_drawer_contacts_list'
};
/**
* Show the loading icon.
*
* @param {Object} body Contacts body container element.
*/
var startLoading = function(body) {
body.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
};
/**
* Hide the loading icon.
*
* @param {Object} body Contacts body container element.
*/
var stopLoading = function(body) {
body.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
};
/**
* Get the content container of the contacts body container element.
*
* @param {Object} body Contacts body container element.
* @return {Object} jQuery element
*/
var getContentContainer = function(body) {
return body.find(SELECTORS.CONTENT_CONTAINER);
};
/**
* Get the contacts container of the contacts body container element.
*
* @param {Object} body Contacts body container element.
* @return {Object} jQuery element
*/
var getContactsContainer = function(body) {
return body.find(SELECTORS.CONTACTS);
};
/**
* Show a message when no contacts found.
*
* @param {Object} body Contacts body container element.
*/
var showEmptyMessage = function(body) {
getContentContainer(body).addClass('hidden');
body.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
};
/**
* Hide the placeholder image.
*
* @param {Object} body Contacts body container element.
*/
var hidePlaceholder = function(body) {
body.find(SELECTORS.PLACEHOLDER).addClass('hidden');
};
/**
* Show the content container.
*
* @param {Object} body Contacts body container element.
*/
var showContent = function(body) {
getContentContainer(body).removeClass('hidden');
};
/**
* Find a contact element.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId User id of contact.
* @return {Object} contact element.
*/
var findContact = function(body, userId) {
return body.find('[data-contact-user-id="' + userId + '"]');
};
/**
* Get logged in userid.
*
* @param {Object} body Contacts body container element.
* @return {Number} Logged in userid.
*/
var getLoggedInUserId = function(body) {
return body.attr('data-user-id');
};
/**
* Render the contacts in the content container.
*
* @param {Object} body Contacts body container element.
* @param {Array} contacts List of contacts.
* @return {Object} jQuery promise
*/
var render = function(body, contacts) {
var contentContainer = getContentContainer(body);
return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts})
.then(function(html) {
hidePlaceholder(body);
contentContainer.append(html);
showContent(body);
return html;
});
};
/**
* Load the user contacts and call the renderer.
*
* @param {Object} body Contacts body container element.
* @return {Object} jQuery promise
*/
var loadContacts = function(body) {
var userId = getLoggedInUserId(body);
return MessageRepository.getContacts(userId, (LOAD_CONTACTS_LIMIT + 1), contactsOffset)
.then(function(result) {
return result.contacts;
})
.then(function(contacts) {
if (contacts.length > LOAD_CONTACTS_LIMIT) {
contacts.pop();
} else {
loadedAllContacts = true;
}
return contacts;
})
.then(function(contacts) {
if (contactsOffset == 0 && contacts.length == 0) {
hidePlaceholder(body);
showEmptyMessage(body);
}
numContacts = numContacts + contacts.length;
contactsOffset = contactsOffset + LOAD_CONTACTS_LIMIT;
if (contacts.length > 0) {
return render(body, contacts);
}
return contacts;
});
};
/**
* Remove contact from view.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var removeContact = function(body, userId) {
findContact(body, userId).remove();
};
/**
* Show the contact has been blocked.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var showContactBlocked = function(body, userId) {
var contact = findContact(body, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden');
}
};
/**
* Show the contact has been unblocked.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var showContactUnblocked = function(body, userId) {
var contact = findContact(body, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden');
}
};
/**
* Listen to and handle events for contacts.
*
* @param {Object} body Contacts body container element.
*/
var registerEventListeners = function(body) {
PubSub.subscribe(Events.CONTACT_ADDED, function() {
contactsOffset = 0;
loadedAllContacts = false;
getContentContainer(body).empty();
loadContacts(body);
});
PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) {
removeContact(body, userId);
});
PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) {
showContactBlocked(body, userId);
});
PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) {
showContactUnblocked(body, userId);
});
var contactsContainer = getContactsContainer(body);
CustomEvents.define(contactsContainer, [
CustomEvents.events.scrollBottom,
CustomEvents.events.scrollLock
]);
contactsContainer.on(CustomEvents.events.scrollBottom, function(e, data) {
var hasContacts = numContacts > 1;
if (!loadedAllContacts && hasContacts && !waitForScrollLoad) {
waitForScrollLoad = true;
startLoading(body);
loadContacts(body)
.then(function() {
stopLoading(body);
waitForScrollLoad = false;
return;
})
.catch(function(error) {
stopLoading(body);
waitForScrollLoad = false;
Notification.exception(error);
});
}
data.originalEvent.preventDefault();
});
};
/**
* Setup the contact page.
*
* @param {Object} header Contacts header container element.
* @param {Object} body Contacts body container element.
*/
var show = function(header, body) {
body = $(body);
contactsOffset = 0;
if (!body.attr('data-contacts-init')) {
registerEventListeners(body);
body.attr('data-contacts-init', true);
}
if (!loadedAllContacts) {
loadContacts(body);
}
};
return {
show: show,
};
});
@@ -0,0 +1,200 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the contacts section of the contacts page.
*
* @module core_message/message_drawer_view_contacts_section_contacts
* @package message
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/pubsub',
'core/templates',
'core_message/message_repository',
'core_message/message_drawer_events',
'core_message/message_drawer_lazy_load_list'
],
function(
$,
Notification,
PubSub,
Templates,
MessageRepository,
Events,
LazyLoadList
) {
var limit = 100;
var offset = 0;
var SELECTORS = {
BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]',
CONTACT: '[data-region="contact"]',
CONTENT_CONTAINER: '[data-region="contacts-content-container"]'
};
var TEMPLATES = {
CONTACTS_LIST: 'core_message/message_drawer_contacts_list'
};
/**
* Find a contact element.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId User id of contact.
* @return {Object} contact element.
*/
var findContact = function(body, userId) {
return body.find('[data-contact-user-id="' + userId + '"]');
};
/**
* Render the contacts in the content container.
*
* @param {Object} contentContainer Content container element.
* @param {Array} contacts List of contacts.
* @return {Object} jQuery promise
*/
var render = function(contentContainer, contacts) {
var formattedContacts = contacts.map(function(contact) {
return $.extend(contact, {id: contact.userid});
});
return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: formattedContacts})
.then(function(html) {
contentContainer.append(html);
return html;
})
.catch(Notification.exception);
};
/**
* Load the user contacts and call the renderer.
*
* @param {Object} listRoot The lazy loaded list root element
* @param {Integer} userId The logged in user id.
* @return {Object} jQuery promise
*/
var load = function(listRoot, userId) {
return MessageRepository.getContacts(userId, (limit + 1), offset)
.then(function(result) {
return result.contacts;
})
.then(function(contacts) {
if (contacts.length > limit) {
contacts.pop();
} else {
LazyLoadList.setLoadedAll(listRoot, true);
}
return contacts;
})
.then(function(contacts) {
offset = offset + limit;
return contacts;
})
.catch(Notification.exception);
};
/**
* Remove contact from view.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var removeContact = function(body, userId) {
findContact(body, userId).remove();
};
/**
* Show the contact has been blocked.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var showContactBlocked = function(body, userId) {
var contact = findContact(body, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden');
}
};
/**
* Show the contact has been unblocked.
*
* @param {Object} body Contacts body container element.
* @param {Number} userId Contact userid.
*/
var showContactUnblocked = function(body, userId) {
var contact = findContact(body, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden');
}
};
/**
* Listen to and handle events for contacts.
*
* @param {Object} root Contacts section container element.
*/
var registerEventListeners = function(root) {
PubSub.subscribe(Events.CONTACT_ADDED, function(profile) {
var listContentContainer = LazyLoadList.getContentContainer(root);
render(listContentContainer, [profile]);
LazyLoadList.hideEmptyMessage(root);
LazyLoadList.showContent(root);
});
PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) {
removeContact(root, userId);
var contacts = root.find(SELECTORS.CONTACT);
if (!contacts.length) {
LazyLoadList.hideContent(root);
LazyLoadList.showEmptyMessage(root);
}
});
PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) {
showContactBlocked(root, userId);
});
PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) {
showContactUnblocked(root, userId);
});
};
/**
* Setup the contacts section.
*
* @param {Object} root Contacts section container.
*/
var show = function(root) {
if (!root.attr('data-contacts-init')) {
registerEventListeners(root);
root.attr('data-contacts-init', true);
}
// The root element is already the lazy loaded list root.
LazyLoadList.show(root, load, render);
};
return {
show: show,
};
});
@@ -0,0 +1,139 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the requests section of the contacts page.
*
* @module core_message/message_drawer_view_contacts_section_requests
* @package message
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/pubsub',
'core/templates',
'core_message/message_repository',
'core_message/message_drawer_events',
'core_message/message_drawer_lazy_load_list'
],
function(
$,
Notification,
PubSub,
Templates,
MessageRepository,
MessageDrawerEvents,
LazyLoadList
) {
var SELECTORS = {
CONTACT_REQUEST: '[data-region="contact-request"]'
};
var TEMPLATES = {
REQUESTS_LIST: 'core_message/message_drawer_view_contacts_body_section_requests_list'
};
/**
* Render the requests in the content container.
*
* @param {Object} contentContainer List container element.
* @param {Array} requests List of requests.
* @return {Object} jQuery promise
*/
var render = function(contentContainer, requests) {
var formattedRequests = requests.map(function(request) {
return {
// This is actually the user id.
id: request.id,
profileimageurl: request.profileimageurl,
fullname: request.fullname
};
});
return Templates.render(TEMPLATES.REQUESTS_LIST, {requests: formattedRequests})
.then(function(html) {
contentContainer.append(html);
return html;
})
.catch(Notification.exception);
};
/**
* Load the user contacts and call the renderer.
*
* @param {Object} listRoot The lazy loaded list root element
* @param {Integer} userId The logged in user id.
* @return {Object} jQuery promise
*/
var load = function(listRoot, userId) {
return MessageRepository.getContactRequests(userId)
.then(function(requests) {
LazyLoadList.setLoadedAll(listRoot, true);
return requests;
})
.catch(Notification.exception);
};
/**
* Handle when a contact request is accepted or declined by removing the contact
* list from the page.
*
* @param {Object} root The section root element
* @return {Function} The event handler function
*/
var handleContactRequestProcessed = function(root) {
return function(request) {
root.find('[data-request-id="' + request.userid + '"]').remove();
var contactRequests = root.find(SELECTORS.CONTACT_REQUEST);
if (!contactRequests.length) {
LazyLoadList.showEmptyMessage(root);
LazyLoadList.hideContent(root);
}
};
};
/**
* Listen for any events that might affect the requests section.
*
* @param {Object} root The section root element
*/
var registerEventListeners = function(root) {
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleContactRequestProcessed(root));
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleContactRequestProcessed(root));
};
/**
* Setup the requests section.
*
* @param {Object} root Requests section container.
*/
var show = function(root) {
if (!root.attr('data-contacts-init')) {
registerEventListeners(root);
root.attr('data-contacts-init', true);
}
// The root element is already the lazy loaded list root.
LazyLoadList.show(root, load, render);
};
return {
show: show,
};
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Constant values for the conversation page in the message drawer.
*
* @module core_message/message_drawer_view_conversation_constants
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
var SELECTORS = {
ACTION_ACCEPT_CONTACT_REQUEST: '[data-action="accept-contact-request"]',
ACTION_CANCEL_CONFIRM: '[data-action="cancel-confirm"]',
ACTION_CANCEL_EDIT_MODE: '[data-action="cancel-edit-mode"]',
ACTION_CONFIRM_ADD_CONTACT: '[data-action="confirm-add-contact"]',
ACTION_CONFIRM_BLOCK: '[data-action="confirm-block"]',
ACTION_CONFIRM_DELETE_SELECTED_MESSAGES: '[data-action="confirm-delete-selected-messages"]',
ACTION_CONFIRM_DELETE_CONVERSATION: '[data-action="confirm-delete-conversation"]',
ACTION_CONFIRM_FAVOURITE: '[data-action="confirm-favourite"]',
ACTION_CONFIRM_UNFAVOURITE: '[data-action="confirm-unfavourite"]',
ACTION_CONFIRM_REMOVE_CONTACT: '[data-action="confirm-remove-contact"]',
ACTION_CONFIRM_UNBLOCK: '[data-action="confirm-unblock"]',
ACTION_DECLINE_CONTACT_REQUEST: '[data-action="decline-contact-request"]',
ACTION_REQUEST_ADD_CONTACT: '[data-action="request-add-contact"]',
ACTION_REQUEST_BLOCK: '[data-action="request-block"]',
ACTION_REQUEST_DELETE_CONVERSATION: '[data-action="request-delete-conversation"]',
ACTION_REQUEST_DELETE_SELECTED_MESSAGES: '[data-action="delete-selected-messages"]',
ACTION_REQUEST_REMOVE_CONTACT: '[data-action="request-remove-contact"]',
ACTION_REQUEST_UNBLOCK: '[data-action="request-unblock"]',
ACTION_VIEW_CONTACT: '[data-action="view-contact"]',
ACTION_VIEW_GROUP_INFO: '[data-action="view-group-info"]',
CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
CONFIRM_DIALOGUE_BUTTON_TEXT: '[data-region="dialogue-button-text"]',
CONFIRM_DIALOGUE_CANCEL_BUTTON: '[data-action="cancel-confirm"]',
CONFIRM_DIALOGUE_CONTAINER: '[data-region="confirm-dialogue-container"]',
CONFIRM_DIALOGUE_HEADER: '[data-region="dialogue-header"]',
CONFIRM_DIALOGUE_TEXT: '[data-region="dialogue-text"]',
CONTACT_REQUEST_SENT_MESSAGE_CONTAINER: '[data-region="contact-request-sent-message-container"]',
CONTENT_PLACEHOLDER_CONTAINER: '[data-region="content-placeholder"]',
CONTENT_CONTAINER: '[data-region="content-container"]',
CONTENT_MESSAGES_CONTAINER: '[data-region="content-message-container"]',
CONTENT_MESSAGES_FOOTER_CONTAINER: '[data-region="content-messages-footer-container"]',
CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER: '[data-region="content-messages-footer-edit-mode-container"]',
CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER: '[data-region="content-messages-footer-require-contact-container"]',
CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER: '[data-region="content-messages-footer-require-unblock-container"]',
CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER: '[data-region="content-messages-footer-unable-to-message"]',
DAY_MESSAGES_CONTAINER: '[data-region="day-messages-container"]',
FAVOURITE_ICON_CONTAINER: '[data-region="favourite-icon-container"]',
FOOTER_CONTAINER: '[data-region="content-messages-footer-container"]',
HEADER: '[data-region="header-content"]',
HEADER_EDIT_MODE: '[data-region="header-edit-mode"]',
HEADER_PLACEHOLDER_CONTAINER: '[data-region="header-placeholder"]',
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
MESSAGE: '[data-region="message"]',
MESSAGE_NOT_SELECTED: '[data-region="message"][aria-checked="false"]',
MESSAGE_NOT_SELECTED_ICON: '[data-region="not-selected-icon"]',
MESSAGE_SELECTED_ICON: '[data-region="selected-icon"]',
MESSAGES: '[data-region="content-message-container"]',
MESSAGES_CONTAINER: '[data-region="content-message-container"]',
MESSAGES_SELECTED_COUNT: '[data-region="message-selected-court"]',
MESSAGE_TEXT_AREA: '[data-region="send-message-txt"]',
MORE_MESSAGES_LOADING_ICON_CONTAINER: '[data-region="more-messages-loading-icon-container"]',
PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]',
SEND_MESSAGE_BUTTON: '[data-action="send-message"]',
SEND_MESSAGE_ICON_CONTAINER: '[data-region="send-icon-container"]',
TEXT: '[data-region="text"]',
TITLE: '[data-region="title"]'
};
var TEMPLATES = {
HEADER_PRIVATE: 'core_message/message_drawer_view_conversation_header_content_type_private',
HEADER_PRIVATE_NO_CONTROLS: 'core_message/message_drawer_view_conversation_header_content_type_private_no_controls',
HEADER_PUBLIC: 'core_message/message_drawer_view_conversation_header_content_type_public',
DAY: 'core_message/message_drawer_view_conversation_body_day',
MESSAGE: 'core_message/message_drawer_view_conversation_body_message',
MESSAGES: 'core_message/message_drawer_view_conversation_body_messages'
};
var CONVERSATION_TYPES = {
PRIVATE: 1,
PUBLIC: 2
};
return {
SELECTORS: SELECTORS,
TEMPLATES: TEMPLATES,
CONVERSATION_TYPES: CONVERSATION_TYPES,
NEWEST_MESSAGES_FIRST: true,
LOAD_MESSAGE_LIMIT: 100,
INITIAL_NEW_MESSAGE_POLL_TIMEOUT: 1000
};
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,655 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* This module operates on the view states from the message_drawer_view_conversation module.
* It exposes functions that can be used to generate new version of the state.
*
* Important notes for this module:
* 1.) The existing state is always immutable. It should never be modified.
* 2.) All functions that operate on the state should always clone the state and
* modify the cloned state before returning it.
*
* It's important that the states remain immutable because they are diff'd in
* the message_drawer_view_conversation_patcher module in order to work out what
* has changed.
*
* @module core_message/message_drawer_view_conversation_state_manager
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery'], function($) {
/**
* Clone a state, a state is a collection of information about the variables required to build
* the conversation user interface.
*
* @param {Object} state State to clone
* @return {Object} newstate A copy of the state to clone.
*/
var cloneState = function(state) {
var newState = $.extend({}, state);
newState.messages = state.messages.map(function(message) {
return $.extend({}, message);
});
newState.members = Object.keys(state.members).reduce(function(carry, id) {
carry[id] = $.extend({}, state.members[id]);
carry[id].contactrequests = state.members[id].contactrequests.map(function(request) {
return $.extend({}, request);
});
return carry;
}, {});
return newState;
};
/**
* Format messages to be used in a state.
*
* @param {Array} messages The messages to format.
* @param {Number} loggedInUserId The logged in user id.
* @param {Array} members The converstation members.
* @return {Array} Formatted messages.
*/
var formatMessages = function(messages, loggedInUserId, members) {
return messages.map(function(message) {
var fromLoggedInUser = message.useridfrom == loggedInUserId;
return {
id: parseInt(message.id, 10),
isRead: message.isread,
fromLoggedInUser: fromLoggedInUser,
userFrom: members[message.useridfrom],
text: message.text,
timeCreated: parseInt(message.timecreated, 10)
};
});
};
/**
* Create an initial (blank) state.
*
* @param {Number} midnight Midnight time.
* @param {Number} loggedInUserId The logged in user id.
* @param {Number} id The conversation id.
* @return {Object} Initial state.
*/
var buildInitialState = function(midnight, loggedInUserId, id) {
return {
midnight: midnight,
loggedInUserId: loggedInUserId,
id: id,
name: null,
subname: null,
type: null,
totalMemberCount: null,
imageUrl: null,
isFavourite: null,
members: {},
messages: [],
hasTriedToLoadMessages: false,
loadingMessages: true,
sendingMessage: false,
loadingMembers: true,
loadingConfirmAction: false,
pendingBlockUserIds: [],
pendingUnblockUserIds: [],
pendingRemoveContactIds: [],
pendingAddContactIds: [],
pendingDeleteMessageIds: [],
pendingDeleteConversation: false,
selectedMessageIds: []
};
};
/**
* Add messages to a state and sort them by timecreated.
*
* @param {Object} state Current state.
* @param {Array} messages Messages to add to state.
* @return {Object} state New state with added messages.
*/
var addMessages = function(state, messages) {
var newState = cloneState(state);
var formattedMessages = formatMessages(messages, state.loggedInUserId, state.members);
var allMessages = state.messages.concat(formattedMessages);
// Sort the messages. Oldest to newest.
allMessages.sort(function(a, b) {
if (a.timeCreated < b.timeCreated) {
return -1;
} else if (a.timeCreated > b.timeCreated) {
return 1;
} else if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
// Filter out any duplicate messages.
newState.messages = allMessages.filter(function(message, index, sortedMessages) {
return !index || message.id !== sortedMessages[index - 1].id;
});
return newState;
};
/**
* Remove messages from state.
*
* @param {Object} state Current state.
* @param {Array} messages Messages to remove from state.
* @return {Object} state New state with removed messages.
*/
var removeMessages = function(state, messages) {
var newState = cloneState(state);
var removeMessageIds = messages.map(function(message) {
return message.id;
});
newState.messages = newState.messages.filter(function(message) {
return removeMessageIds.indexOf(message.id) < 0;
});
return newState;
};
/**
* Remove messages from state by message id.
*
* @param {Object} state Current state.
* @param {Array} messagesIds Message ids to remove from state.
* @return {Object} state New state with removed messages.
*/
var removeMessagesById = function(state, messagesIds) {
var newState = cloneState(state);
newState.messages = newState.messages.filter(function(message) {
return messagesIds.indexOf(message.id) < 0;
});
return newState;
};
/**
* Add conversation member to state.
*
* @param {Object} state Current state.
* @param {Array} members Conversation members to be added to state.
* @return {Object} New state with added members.
*/
var addMembers = function(state, members) {
var newState = cloneState(state);
members.forEach(function(member) {
newState.members[member.id] = member;
});
return newState;
};
/**
* Remove members from state.
*
* @param {Object} state Current state.
* @param {Array} members Members to be removed from state.
* @return {Object} New state with removed members.
*/
var removeMembers = function(state, members) {
var newState = cloneState(state);
members.forEach(function(member) {
delete newState.members[member.id];
});
return newState;
};
/**
* Set the state loading messages attribute.
*
* @param {Object} state Current state.
* @param {Bool} value New loading messages value.
* @return {Object} New state with loading messages attribute.
*/
var setLoadingMessages = function(state, value) {
var newState = cloneState(state);
newState.loadingMessages = value;
if (state.loadingMessages && !value) {
// If we're going from loading to not loading then
// it means we've tried to load.
newState.hasTriedToLoadMessages = true;
}
return newState;
};
/**
* Set the state sending message attribute.
*
* @param {Object} state Current state.
* @param {Bool} value New sending message value.
* @return {Object} New state with sending message attribute.
*/
var setSendingMessage = function(state, value) {
var newState = cloneState(state);
newState.sendingMessage = value;
return newState;
};
/**
* Set the state loading members attribute.
*
* @param {Object} state Current state.
* @param {Bool} value New loading members value.
* @return {Object} New state with loading members attribute.
*/
var setLoadingMembers = function(state, value) {
var newState = cloneState(state);
newState.loadingMembers = value;
return newState;
};
/**
* Set the conversation id.
*
* @param {Object} state Current state.
* @param {String} value The ID.
* @return {Object} New state.
*/
var setId = function(state, value) {
var newState = cloneState(state);
newState.id = value;
return newState;
};
/**
* Set the state name attribute.
*
* @param {Object} state Current state.
* @param {String} value New name value.
* @return {Object} New state with name attribute.
*/
var setName = function(state, value) {
var newState = cloneState(state);
newState.name = value;
return newState;
};
/**
* Set the state subname attribute.
*
* @param {Object} state Current state.
* @param {String} value New subname value.
* @return {Object} New state.
*/
var setSubname = function(state, value) {
var newState = cloneState(state);
newState.subname = value;
return newState;
};
/**
* Set the conversation type.
*
* @param {Object} state Current state.
* @param {Int} type Conversation type.
* @return {Object} New state.
*/
var setType = function(state, type) {
var newState = cloneState(state);
newState.type = type;
return newState;
};
/**
* Set whether the conversation is a favourite conversation.
*
* @param {Object} state Current state.
* @param {Bool} isFavourite If it's a favourite.
* @return {Object} New state.
*/
var setIsFavourite = function(state, isFavourite) {
var newState = cloneState(state);
newState.isFavourite = isFavourite;
return newState;
};
/**
* Set the total member count.
*
* @param {Object} state Current state.
* @param {String} count The count.
* @return {Object} New state.
*/
var setTotalMemberCount = function(state, count) {
var newState = cloneState(state);
newState.totalMemberCount = count;
return newState;
};
/**
* Set the conversation image url.
*
* @param {Object} state Current state.
* @param {String} url The url to the image.
* @return {Object} New state.
*/
var setImageUrl = function(state, url) {
var newState = cloneState(state);
newState.imageUrl = url;
return newState;
};
/**
* Set the state loading confirm action attribute.
*
* @param {Object} state Current state.
* @param {Bool} value New loading confirm action value.
* @return {Object} New state with loading confirm action attribute.
*/
var setLoadingConfirmAction = function(state, value) {
var newState = cloneState(state);
newState.loadingConfirmAction = value;
return newState;
};
/**
* Set the state pending delete conversation attribute.
*
* @param {Object} state Current state.
* @param {Bool} value New pending delete conversation value.
* @return {Object} New state with pending delete conversation attribute.
*/
var setPendingDeleteConversation = function(state, value) {
var newState = cloneState(state);
newState.pendingDeleteConversation = value;
return newState;
};
/**
* Set the state pending block userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to block.
* @return {Object} New state with array of pending block userids.
*/
var addPendingBlockUsersById = function(state, userIds) {
var newState = cloneState(state);
userIds.forEach(function(id) {
newState.pendingBlockUserIds.push(id);
});
return newState;
};
/**
* Set the state pending remove userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to remove.
* @return {Object} New state with array of pending remove userids.
*/
var addPendingRemoveContactsById = function(state, userIds) {
var newState = cloneState(state);
userIds.forEach(function(id) {
newState.pendingRemoveContactIds.push(id);
});
return newState;
};
/**
* Set the state pending unblock userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to unblock.
* @return {Object} New state with array of pending unblock userids.
*/
var addPendingUnblockUsersById = function(state, userIds) {
var newState = cloneState(state);
userIds.forEach(function(id) {
newState.pendingUnblockUserIds.push(id);
});
return newState;
};
/**
* Set the state pending add users to contacts userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to add users to contacts.
* @return {Object} New state with array of pending add users to contacts userids.
*/
var addPendingAddContactsById = function(state, userIds) {
var newState = cloneState(state);
userIds.forEach(function(id) {
newState.pendingAddContactIds.push(id);
});
return newState;
};
/**
* Set the state pending delete messages.
*
* @param {Object} state Current state.
* @param {Array} messageIds Messages to delete.
* @return {Object} New state with array of pending delete message ids.
*/
var addPendingDeleteMessagesById = function(state, messageIds) {
var newState = cloneState(state);
messageIds.forEach(function(id) {
newState.pendingDeleteMessageIds.push(id);
});
return newState;
};
/**
* Update the state pending block userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to remove from the list of user ids to block.
* @return {Object} New state with array of pending block userids.
*/
var removePendingBlockUsersById = function(state, userIds) {
var newState = cloneState(state);
newState.pendingBlockUserIds = newState.pendingBlockUserIds.filter(function(id) {
return userIds.indexOf(id) < 0;
});
return newState;
};
/**
* Update the state pending remove userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to remove from the list of user ids to remove.
* @return {Object} New state with array of pending remove userids.
*/
var removePendingRemoveContactsById = function(state, userIds) {
var newState = cloneState(state);
newState.pendingRemoveContactIds = newState.pendingRemoveContactIds.filter(function(id) {
return userIds.indexOf(id) < 0;
});
return newState;
};
/**
* Update the state pending unblock userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to remove from the list of user ids to unblock.
* @return {Object} New state with array of pending unblock userids.
*/
var removePendingUnblockUsersById = function(state, userIds) {
var newState = cloneState(state);
newState.pendingUnblockUserIds = newState.pendingUnblockUserIds.filter(function(id) {
return userIds.indexOf(id) < 0;
});
return newState;
};
/**
* Update the state pending add to contacts userids.
*
* @param {Object} state Current state.
* @param {Array} userIds User ids to remove from the list of user ids to add to contacts.
* @return {Object} New state with array of pending add to contacts userids.
*/
var removePendingAddContactsById = function(state, userIds) {
var newState = cloneState(state);
newState.pendingAddContactIds = newState.pendingAddContactIds.filter(function(id) {
return userIds.indexOf(id) < 0;
});
return newState;
};
/**
* Update the state pending delete messages userids.
*
* @param {Object} state Current state.
* @param {Array} messageIds Message ids to remove from the list of messages to delete.
* @return {Object} New state with array of messages to delete.
*/
var removePendingDeleteMessagesById = function(state, messageIds) {
var newState = cloneState(state);
newState.pendingDeleteMessageIds = newState.pendingDeleteMessageIds.filter(function(id) {
return messageIds.indexOf(id) < 0;
});
return newState;
};
/**
* Add messages to state selected messages.
*
* @param {Object} state Current state.
* @param {Array} messageIds Messages that are selected.
* @return {Object} New state with array of not blocked members.
*/
var addSelectedMessagesById = function(state, messageIds) {
var newState = cloneState(state);
newState.selectedMessageIds = newState.selectedMessageIds.concat(messageIds);
return newState;
};
/**
* Remove messages from the state selected messages.
*
* @param {Object} state Current state.
* @param {Array} messageIds Messages to remove from selected messages.
* @return {Object} New state with array of selected messages.
*/
var removeSelectedMessagesById = function(state, messageIds) {
var newState = cloneState(state);
newState.selectedMessageIds = newState.selectedMessageIds.filter(function(id) {
return messageIds.indexOf(id) < 0;
});
return newState;
};
/**
* Mark messages as read.
*
* @param {Object} state Current state.
* @param {Array} readMessages Messages that are read.
* @return {Object} New state with array of messages that have the isread attribute set.
*/
var markMessagesAsRead = function(state, readMessages) {
var newState = cloneState(state);
var readMessageIds = readMessages.map(function(message) {
return message.id;
});
newState.messages = newState.messages.map(function(message) {
if (readMessageIds.indexOf(message.id) >= 0) {
message.isRead = true;
}
return message;
});
return newState;
};
/**
* Add a contact request to each of the members that the request is for.
*
* @param {Object} state Current state.
* @param {Array} requests The contact requests
* @return {Object} New state
*/
var addContactRequests = function(state, requests) {
var newState = cloneState(state);
requests.forEach(function(request) {
var fromUserId = request.userid;
var toUserId = request.requesteduserid;
newState.members[fromUserId].contactrequests.push(request);
newState.members[toUserId].contactrequests.push(request);
});
return newState;
};
/**
* Remove a contact request from the members of that request.
*
* @param {Object} state Current state.
* @param {Array} requests The contact requests
* @return {Object} New state
*/
var removeContactRequests = function(state, requests) {
var newState = cloneState(state);
requests.forEach(function(request) {
var fromUserId = request.userid;
var toUserId = request.requesteduserid;
newState.members[fromUserId].contactrequests = newState.members[fromUserId].contactrequests.filter(function(existing) {
return existing.userid != fromUserId;
});
newState.members[toUserId].contactrequests = newState.members[toUserId].contactrequests.filter(function(existing) {
return existing.requesteduserid != toUserId;
});
});
return newState;
};
return {
buildInitialState: buildInitialState,
addMessages: addMessages,
removeMessages: removeMessages,
removeMessagesById: removeMessagesById,
addMembers: addMembers,
removeMembers: removeMembers,
setLoadingMessages: setLoadingMessages,
setSendingMessage: setSendingMessage,
setLoadingMembers: setLoadingMembers,
setId: setId,
setName: setName,
setSubname: setSubname,
setType: setType,
setIsFavourite: setIsFavourite,
setTotalMemberCount: setTotalMemberCount,
setImageUrl: setImageUrl,
setLoadingConfirmAction: setLoadingConfirmAction,
setPendingDeleteConversation: setPendingDeleteConversation,
addPendingBlockUsersById: addPendingBlockUsersById,
addPendingRemoveContactsById: addPendingRemoveContactsById,
addPendingUnblockUsersById: addPendingUnblockUsersById,
addPendingAddContactsById: addPendingAddContactsById,
addPendingDeleteMessagesById: addPendingDeleteMessagesById,
removePendingBlockUsersById: removePendingBlockUsersById,
removePendingRemoveContactsById: removePendingRemoveContactsById,
removePendingUnblockUsersById: removePendingUnblockUsersById,
removePendingAddContactsById: removePendingAddContactsById,
removePendingDeleteMessagesById: removePendingDeleteMessagesById,
addSelectedMessagesById: addSelectedMessagesById,
removeSelectedMessagesById: removeSelectedMessagesById,
markMessagesAsRead: markMessagesAsRead,
addContactRequests: addContactRequests,
removeContactRequests: removeContactRequests
};
});
@@ -0,0 +1,170 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the group info page of the message drawer.
*
* @module core_message/message_drawer_view_group_info
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/str',
'core/templates',
'core_message/message_repository',
'core_message/message_drawer_lazy_load_list',
],
function(
$,
Str,
Templates,
Repository,
LazyLoadList
) {
var LOAD_MEMBERS_LIMIT = 50;
var SELECTORS = {
CONTENT_CONTAINER: '[data-region="group-info-content-container"]',
MEMBERS_LIST: '[data-region="members-list"]',
};
var TEMPLATES = {
CONTENT: 'core_message/message_drawer_view_group_info_body_content',
MEMBERS_LIST: 'core_message/message_drawer_view_group_info_participants_list'
};
/**
* Get the content container of the group info view container.
*
* @param {Object} root Contact container element.
* @return {Object} jQuery object
*/
var getContentContainer = function(root) {
return root.find(SELECTORS.CONTENT_CONTAINER);
};
/**
* Render the group info page.
*
* @param {Object} root Container element.
* @param {Object} conversation The group conversation.
* @param {Number} loggedInUserId The logged in user's id.
* @return {Object} jQuery promise
*/
var render = function(root, conversation, loggedInUserId) {
var placeholderCount = conversation.totalMemberCount > 50 ? 50 : conversation.totalMemberCount;
var placeholders = Array.apply(null, Array(placeholderCount)).map(function() {
return true;
});
var templateContext = {
name: conversation.name,
subname: conversation.subname,
imageurl: conversation.imageUrl,
placeholders: placeholders,
loggedinuser: {
id: loggedInUserId
}
};
return Templates.render(TEMPLATES.CONTENT, templateContext)
.then(function(html) {
getContentContainer(root).append(html);
return html;
});
};
/**
* Get the callback to load members of the conversation.
*
* @param {Object} conversation The conversation
* @param {Number} limit How many members to load
* @param {Number} offset How many memebers to skip
* @return {Function} the callback.
*/
var getLoadMembersCallback = function(conversation, limit, offset) {
return function(root, userId) {
return Repository.getConversationMembers(conversation.id, userId, limit + 1, offset)
.then(function(members) {
if (members.length > limit) {
members = members.slice(0, -1);
} else {
LazyLoadList.setLoadedAll(root, true);
}
offset = offset + limit;
return members;
});
};
};
/**
* Function to render the members in the list.
*
* @param {Object} contentContainer The list content container.
* @param {Array} members The list of members to render
* @return {Object} jQuery promise
*/
var renderMembersCallback = function(contentContainer, members) {
return Templates.render(TEMPLATES.MEMBERS_LIST, {contacts: members})
.then(function(html) {
contentContainer.append(html);
return html;
});
};
/**
* Setup the contact page.
*
* @param {Object} root Contact container element.
* @param {Number} conversation The conversation
* @param {Number} loggedInUserId The logged in user id
* @return {Object} jQuery promise
*/
var show = function(root, conversation, loggedInUserId) {
root = $(root);
getContentContainer(root).empty();
return render(root, conversation, loggedInUserId)
.then(function() {
var listRoot = LazyLoadList.getRoot(root);
LazyLoadList.show(
listRoot,
getLoadMembersCallback(conversation, LOAD_MEMBERS_LIMIT, 0),
renderMembersCallback
);
return;
});
};
/**
* String describing this page used for aria-labels.
*
* @param {Object} root Contact container element.
* @param {Number} conversation The conversation
* @return {Object} jQuery promise
*/
var description = function(root, conversation) {
return Str.get_string('messagedrawerviewgroupinfo', 'core_message', conversation.name);
};
return {
show: show,
description: description
};
});
@@ -0,0 +1,145 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the overview page of the message drawer.
*
* @module core_message/message_drawer_view_overview
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/key_codes',
'core/pubsub',
'core/str',
'core_message/message_drawer_view_overview_section_favourites',
'core_message/message_drawer_view_overview_section_group_messages',
'core_message/message_drawer_view_overview_section_messages',
'core_message/message_drawer_router',
'core_message/message_drawer_routes',
'core_message/message_drawer_events'
],
function(
$,
KeyCodes,
PubSub,
Str,
Favourites,
GroupMessages,
Messages,
Router,
Routes,
MessageDrawerEvents
) {
var SELECTORS = {
CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]',
FAVOURITES: '[data-region="view-overview-favourites"]',
GROUP_MESSAGES: '[data-region="view-overview-group-messages"]',
MESSAGES: '[data-region="view-overview-messages"]',
SEARCH_INPUT: '[data-region="view-overview-search-input"]'
};
/**
* Get the search input text element.
*
* @param {Object} header Overview header container element.
* @return {Object} The search input element.
*/
var getSearchInput = function(header) {
return header.find(SELECTORS.SEARCH_INPUT);
};
/**
* Decrement the contact request count. If the count is zero or below then
* hide the count.
*
* @param {Object} header Conversation header container element.
* @return {Function} A function to handle decrementing the count.
*/
var decrementContactRequestCount = function(header) {
return function() {
var countContainer = header.find(SELECTORS.CONTACT_REQUEST_COUNT);
var count = parseInt(countContainer.text(), 10);
count = isNaN(count) ? 0 : count - 1;
if (count <= 0) {
countContainer.addClass('hidden');
} else {
countContainer.text(count);
}
};
};
/**
* Listen to, and handle event in the overview header.
*
* @param {Object} header Conversation header container element.
*/
var registerEventListeners = function(header) {
var searchInput = getSearchInput(header);
var ignoredKeys = [KeyCodes.tab, KeyCodes.shift, KeyCodes.ctrl, KeyCodes.alt];
searchInput.on('click', function() {
Router.go(Routes.VIEW_SEARCH);
});
searchInput.on('keydown', function(e) {
if (ignoredKeys.indexOf(e.keyCode) < 0 && e.key != 'Meta') {
Router.go(Routes.VIEW_SEARCH);
}
});
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header));
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header));
};
/**
* Setup the overview page.
*
* @param {Object} header Overview header container element.
* @param {Object} body Overview body container element.
* @return {Object} jQuery promise
*/
var show = function(header, body) {
if (!header.attr('data-init')) {
registerEventListeners(header);
header.attr('data-init', true);
}
getSearchInput(header).val('');
return $.when(
Favourites.show(body.find(SELECTORS.FAVOURITES)),
GroupMessages.show(body.find(SELECTORS.GROUP_MESSAGES)),
Messages.show(body.find(SELECTORS.MESSAGES))
);
};
/**
* String describing this page used for aria-labels.
*
* @return {Object} jQuery promise
*/
var description = function() {
return Str.get_string('messagedrawerviewoverview', 'core_message');
};
return {
show: show,
description: description
};
});
@@ -0,0 +1,543 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls a section of the overview page in the message drawer.
*
* @module core_message/message_drawer_view_overview_section
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events',
'core/notification',
'core/pubsub',
'core/str',
'core/templates',
'core/user_date',
'core_message/message_repository',
'core_message/message_drawer_events',
'core_message/message_drawer_router',
'core_message/message_drawer_routes',
'core_message/message_drawer_lazy_load_list',
'core_message/message_drawer_view_conversation_constants'
],
function(
$,
CustomEvents,
Notification,
PubSub,
Str,
Templates,
UserDate,
MessageRepository,
MessageDrawerEvents,
MessageDrawerRouter,
MessageDrawerRoutes,
LazyLoadList,
MessageDrawerViewConversationContants
) {
var SELECTORS = {
TOGGLE: '[data-region="toggle"]',
CONVERSATION: '[data-conversation-id]',
BLOCKED_ICON_CONTAINER: '[data-region="contact-icon-blocked"]',
LAST_MESSAGE: '[data-region="last-message"]',
LAST_MESSAGE_DATE: '[data-region="last-message-date"]',
UNREAD_COUNT: '[data-region="unread-count"]',
SECTION_TOTAL_COUNT: '[data-region="section-total-count"]',
SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]'
};
var TEMPLATES = {
CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list'
};
var LOAD_LIMIT = 50;
var loadedConversationsById = {};
/**
* Get the section visibility status.
*
* @param {Object} root The section container element.
* @return {Bool} Is section visible.
*/
var isVisible = function(root) {
return LazyLoadList.getRoot(root).hasClass('show');
};
/**
* Set this section as expanded.
*
* @param {Object} root The section container element.
*/
var setExpanded = function(root) {
root.addClass('expanded');
};
/**
* Set this section as collapsed.
*
* @param {Object} root The section container element.
*/
var setCollapsed = function(root) {
root.removeClass('expanded');
};
/**
* Render the messages in the overview page.
*
* @param {Object} contentContainer Conversations content container.
* @param {Array} conversations List of conversations to render.
* @param {Number} userId Logged in user id.
* @return {Object} jQuery promise.
*/
var render = function(contentContainer, conversations, userId) {
var formattedConversations = conversations.map(function(conversation) {
var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
var formattedConversation = {
id: conversation.id,
imageurl: conversation.imageurl,
name: conversation.name,
subname: conversation.subname,
unreadcount: conversation.unreadcount,
lastmessagedate: lastMessage ? lastMessage.timecreated : null,
sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
};
if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
var otherUser = conversation.members.reduce(function(carry, member) {
if (!carry && member.id != userId) {
carry = member;
}
return carry;
}, null);
formattedConversation.userid = otherUser.id;
formattedConversation.showonlinestatus = otherUser.showonlinestatus;
formattedConversation.isonline = otherUser.isonline;
formattedConversation.isblocked = otherUser.isblocked;
}
return formattedConversation;
});
return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations})
.then(function(html) {
contentContainer.append(html);
return html;
})
.catch(Notification.exception);
};
/**
* Build the callback to load conversations.
*
* @param {Number} type The conversation type.
* @param {Bool} includeFavourites Include/exclude favourites.
* @param {Number} offset Result offset
* @return {Function}
*/
var getLoadCallback = function(type, includeFavourites, offset) {
return function(root, userId) {
return MessageRepository.getConversations(
userId,
type,
LOAD_LIMIT + 1,
offset,
includeFavourites
)
.then(function(response) {
var conversations = response.conversations;
if (conversations.length > LOAD_LIMIT) {
conversations = conversations.slice(0, -1);
} else {
LazyLoadList.setLoadedAll(root, true);
}
offset = offset + LOAD_LIMIT;
conversations.forEach(function(conversation) {
loadedConversationsById[conversation.id] = conversation;
});
return conversations;
})
.catch(Notification.exception);
};
};
/**
* Get the total count container element.
*
* @param {Object} root Overview messages container element.
* @return {Object} Total count container element.
*/
var getTotalConversationCountElement = function(root) {
return root.find(SELECTORS.SECTION_TOTAL_COUNT);
};
/**
* Get the unread conversations count container element.
*
* @param {Object} root Overview messages container element.
* @return {Object} Unread conversations count container element.
*/
var getTotalUnreadConversationCountElement = function(root) {
return root.find(SELECTORS.SECTION_UNREAD_COUNT);
};
/**
* Increment the total conversations count.
*
* @param {Object} root Overview messages container element.
*/
var incrementTotalConversationCount = function(root) {
var element = getTotalConversationCountElement(root);
var count = parseInt(element.text());
count = count + 1;
element.text(count);
};
/**
* Decrement the total conversations count.
*
* @param {Object} root Overview messages container element.
*/
var decrementTotalConversationCount = function(root) {
var element = getTotalConversationCountElement(root);
var count = parseInt(element.text());
count = count - 1;
element.text(count);
};
/**
* Decrement the total unread conversations count.
*
* @param {Object} root Overview messages container element.
*/
var decrementTotalUnreadConversationCount = function(root) {
var element = getTotalUnreadConversationCountElement(root);
var count = parseInt(element.text());
count = count - 1;
element.text(count);
if (count < 1) {
element.addClass('hidden');
}
};
/**
* Get a contact / conversation element.
*
* @param {Object} root Overview messages container element.
* @param {Number} conversationId The conversation id.
* @return {Object} Conversation element.
*/
var getConversationElement = function(root, conversationId) {
return root.find('[data-conversation-id="' + conversationId + '"]');
};
/**
* Get a contact / conversation element from a user id.
*
* @param {Object} root Overview messages container element.
* @param {Number} userId The user id.
* @return {Object} Conversation element.
*/
var getConversationElementFromUserId = function(root, userId) {
return root.find('[data-user-id="' + userId + '"]');
};
/**
* Show the contact is blocked icon.
*
* @param {Object} conversationElement The conversation element.
*/
var blockContact = function(conversationElement) {
conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden');
};
/**
* Hide the contact is blocked icon.
*
* @param {Object} conversationElement The conversation element.
*/
var unblockContact = function(conversationElement) {
conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden');
};
/**
* Update the last message from / to a contact.
*
* @param {Object} element Conversation element.
* @param {Object} conversation The conversation.
* @return {Object} jQuery promise
*/
var updateLastMessage = function(element, conversation) {
var message = conversation.messages[conversation.messages.length - 1];
var youString = '';
var stringRequests = [
{key: 'you', component: 'core_message'},
{key: 'strftimetime24', component: 'core_langconfig'},
];
return Str.get_strings(stringRequests)
.then(function(strings) {
youString = strings[0];
return UserDate.get([{timestamp: message.timeCreated, format: strings[1]}]);
})
.then(function(dates) {
return dates[0];
})
.then(function(dateString) {
var lastMessage = $(message.text).text();
if (message.fromLoggedInUser) {
lastMessage = youString + ' ' + lastMessage;
}
element.find(SELECTORS.LAST_MESSAGE).html(lastMessage);
element.find(SELECTORS.LAST_MESSAGE_DATE).text(dateString).removeClass('hidden');
return dateString;
});
};
/**
* Create an render new conversation element in the list of conversations.
*
* @param {Object} root Overview messages container element.
* @param {Object} conversation The conversation.
* @return {Object} jQuery promise
*/
var createNewConversation = function(root, conversation) {
var existingConversations = root.find(SELECTORS.CONVERSATION);
var text = '';
if (!existingConversations.length) {
// If we didn't have any conversations then we need to show
// the content of the list and hide the empty message.
var listRoot = LazyLoadList.getRoot(root);
LazyLoadList.showContent(listRoot);
LazyLoadList.hideEmptyMessage(listRoot);
}
var messageCount = conversation.messages.length;
var lastMessage = messageCount ? conversation.messages[messageCount - 1] : null;
if (lastMessage) {
text = $(lastMessage.text).text() || lastMessage.text;
}
var formattedConversation = {
id: conversation.id,
name: conversation.name,
subname: conversation.subname,
lastmessagedate: lastMessage ? lastMessage.timeCreated : null,
sentfromcurrentuser: lastMessage ? lastMessage.fromLoggedInUser : null,
lastmessage: text,
imageurl: conversation.imageUrl,
};
// Cache the conversation.
loadedConversationsById[conversation.id] = conversation;
return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: [formattedConversation]})
.then(function(html) {
var contentContainer = LazyLoadList.getContentContainer(root);
return contentContainer.prepend(html);
})
.then(function() {
return incrementTotalConversationCount(root);
})
.catch(Notification.exception);
};
/**
* Delete a conversation from the list of conversations.
*
* @param {Object} root Overview messages container element.
* @param {Object} conversationElement The conversation element.
*/
var deleteConversation = function(root, conversationElement) {
conversationElement.remove();
decrementTotalConversationCount(root);
var conversations = root.find(SELECTORS.CONVERSATION);
if (!conversations.length) {
// If we don't have any conversations then we need to hide
// the content of the list and show the empty message.
var listRoot = LazyLoadList.getRoot(root);
LazyLoadList.hideContent(listRoot);
LazyLoadList.showEmptyMessage(listRoot);
}
};
/**
* Mark a conversation as read.
*
* @param {Object} root Overview messages container element.
* @param {Object} conversationElement The conversation element.
*/
var markConversationAsRead = function(root, conversationElement) {
var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT);
unreadCount.text('0');
unreadCount.addClass('hidden');
decrementTotalUnreadConversationCount(root);
};
/**
* Listen to, and handle events in this section.
*
* @param {Object} root The section container element.
* @param {Function} loadCallback The callback to load items.
* @param {Number} type The conversation type for this section
* @param {Bool} includeFavourites If this section includes favourites
*/
var registerEventListeners = function(root, loadCallback, type, includeFavourites) {
var listRoot = LazyLoadList.getRoot(root);
// Set the minimum height of the section to the height of the toggle. This
// smooths out the collapse animation.
var toggle = root.find(SELECTORS.TOGGLE);
root.css('min-height', toggle.outerHeight());
root.on('show.bs.collapse', function() {
setExpanded(root);
LazyLoadList.show(listRoot, loadCallback, render);
});
root.on('hidden.bs.collapse', function() {
setCollapsed(root);
});
PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(userId) {
var conversationElement = getConversationElementFromUserId(root, userId);
if (conversationElement.length) {
blockContact(conversationElement);
}
});
PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
var conversationElement = getConversationElementFromUserId(root, userId);
if (conversationElement.length) {
unblockContact(conversationElement);
}
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
if (
(type && conversation.type != type) ||
(includeFavourites && !conversation.isFavourite) ||
(!includeFavourites && conversation.isFavourite)
) {
return;
}
var conversationId = conversation.id;
var element = getConversationElement(root, conversationId);
if (element.length) {
updateLastMessage(element, conversation);
} else {
createNewConversation(root, conversation);
}
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) {
var conversationElement = getConversationElement(root, conversationId);
if (conversationElement.length) {
deleteConversation(root, conversationElement);
}
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) {
var conversationElement = getConversationElement(root, conversationId);
if (conversationElement.length) {
markConversationAsRead(root, conversationElement);
}
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
var conversationElement = null;
if (includeFavourites && (!type || type == conversation.type)) {
conversationElement = getConversationElement(root, conversation.id);
if (!conversationElement.length) {
createNewConversation(root, conversation);
}
} else if (type == conversation.type) {
conversationElement = getConversationElement(root, conversation.id);
if (conversationElement.length) {
deleteConversation(root, conversationElement);
}
}
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
var conversationElement = null;
if (includeFavourites) {
conversationElement = getConversationElement(root, conversation.id);
if (conversationElement.length) {
deleteConversation(root, conversationElement);
}
} else if (type == conversation.type) {
conversationElement = getConversationElement(root, conversation.id);
if (!conversationElement.length) {
createNewConversation(root, conversation);
}
}
});
CustomEvents.define(root, [CustomEvents.events.activate]);
root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) {
var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
var conversationId = conversationElement.attr('data-conversation-id');
var conversation = loadedConversationsById[conversationId];
MessageDrawerRouter.go(MessageDrawerRoutes.VIEW_CONVERSATION, conversation);
data.originalEvent.preventDefault();
});
};
/**
* Setup the section.
*
* @param {Object} root The section container element.
* @param {Number} type The conversation type for this section
* @param {Bool} includeFavourites If this section includes favourites
*/
var show = function(root, type, includeFavourites) {
root = $(root);
if (!root.attr('data-init')) {
var loadCallback = getLoadCallback(type, includeFavourites, 0);
registerEventListeners(root, loadCallback, type, includeFavourites);
if (isVisible(root)) {
setExpanded(root);
var listRoot = LazyLoadList.getRoot(root);
LazyLoadList.show(listRoot, loadCallback, render);
}
root.attr('data-init', true);
}
};
return {
show: show
};
});
@@ -0,0 +1,48 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the favourites section of the overview page in the message drawer.
*
* @module core_message/message_drawer_view_overview_section_favourites
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core_message/message_drawer_view_overview_section'
],
function(
$,
Section
) {
// All conversation types.
var CONVERSATION_TYPE = null;
var INCLUDE_FAVOURITES = true;
/**
* Show the overview page conversations.
*
* @param {Object} root Overview messages container element.
*/
var show = function(root) {
Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
};
return {
show: show,
};
});
@@ -0,0 +1,49 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the group overview section of the overview page in the message drawer.
*
* @module core_message/message_drawer_view_overview_section_group_messages
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core_message/message_drawer_view_overview_section'
],
function(
$,
Section
) {
// Public conversations.
var CONVERSATION_TYPE = 2;
var INCLUDE_FAVOURITES = false;
/**
* Show the overview page conversations.
*
* @param {Object} root Overview messages container element.
*/
var show = function(root) {
root = $(root);
Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
};
return {
show: show,
};
});
@@ -0,0 +1,48 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the messages section of the overview page in the message drawer.
*
* @module core_message/message_drawer_view_overview_section_messages
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core_message/message_drawer_view_overview_section'
],
function(
$,
Section
) {
// Private conversations.
var CONVERSATION_TYPE = 1;
var INCLUDE_FAVOURITES = false;
/**
* Show the overview page conversations.
*
* @param {Object} root Overview messages container element.
*/
var show = function(root) {
Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
};
return {
show: show,
};
});
@@ -0,0 +1,837 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the search page of the message drawer.
*
* @module core_message/message_drawer_view_search
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events',
'core/notification',
'core/pubsub',
'core/str',
'core/templates',
'core_message/message_repository',
'core_message/message_drawer_events',
],
function(
$,
CustomEvents,
Notification,
PubSub,
Str,
Templates,
Repository,
Events
) {
var MESSAGE_SEARCH_LIMIT = 50;
var USERS_SEARCH_LIMIT = 50;
var USERS_INITIAL_SEARCH_LIMIT = 3;
var SELECTORS = {
BLOCK_ICON_CONTAINER: '[data-region="block-icon-container"]',
CANCEL_SEARCH_BUTTON: '[data-action="cancel-search"]',
CONTACTS_CONTAINER: '[data-region="contacts-container"]',
CONTACTS_LIST: '[data-region="contacts-container"] [data-region="list"]',
EMPTY_MESSAGE_CONTAINER: '[data-region="empty-message-container"]',
LIST: '[data-region="list"]',
LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
LOADING_PLACEHOLDER: '[data-region="loading-placeholder"]',
MESSAGES_LIST: '[data-region="messages-container"] [data-region="list"]',
MESSAGES_CONTAINER: '[data-region="messages-container"]',
NON_CONTACTS_CONTAINER: '[data-region="non-contacts-container"]',
NON_CONTACTS_LIST: '[data-region="non-contacts-container"] [data-region="list"]',
SEARCH_ICON_CONTAINER: '[data-region="search-icon-container"]',
SEARCH_ACTION: '[data-action="search"]',
SEARCH_INPUT: '[data-region="search-input"]',
SEARCH_RESULTS_CONTAINER: '[data-region="search-results-container"]',
LOAD_MORE_USERS: '[data-action="load-more-users"]',
LOAD_MORE_MESSAGES: '[data-action="load-more-messages"]',
BUTTON_TEXT: '[data-region="button-text"]',
NO_RESULTS_CONTAINTER: '[data-region="no-results-container"]',
};
var TEMPLATES = {
CONTACTS_LIST: 'core_message/message_drawer_contacts_list',
NON_CONTACTS_LIST: 'core_message/message_drawer_non_contacts_list',
MESSAGES_LIST: 'core_message/message_drawer_messages_list'
};
/**
* Get the logged in user id.
*
* @param {Object} body Search body container element.
* @return {Number} User id.
*/
var getLoggedInUserId = function(body) {
return body.attr('data-user-id');
};
/**
* Show the no messages container element.
*
* @param {Object} body Search body container element.
* @return {Object} No messages container element.
*/
var getEmptyMessageContainer = function(body) {
return body.find(SELECTORS.EMPTY_MESSAGE_CONTAINER);
};
/**
* Get the search loading icon.
*
* @param {Object} header Search header container element.
* @return {Object} Loading icon element.
*/
var getLoadingIconContainer = function(header) {
return header.find(SELECTORS.LOADING_ICON_CONTAINER);
};
/**
* Get the loading container element.
*
* @param {Object} body Search body container element.
* @return {Object} Loading container element.
*/
var getLoadingPlaceholder = function(body) {
return body.find(SELECTORS.LOADING_PLACEHOLDER);
};
/**
* Get the search icon container.
*
* @param {Object} header Search header container element.
* @return {Object} Search icon container.
*/
var getSearchIconContainer = function(header) {
return header.find(SELECTORS.SEARCH_ICON_CONTAINER);
};
/**
* Get the search input container.
*
* @param {Object} header Search header container element.
* @return {Object} Search input container.
*/
var getSearchInput = function(header) {
return header.find(SELECTORS.SEARCH_INPUT);
};
/**
* Get the search results container.
*
* @param {Object} body Search body container element.
* @return {Object} Search results container.
*/
var getSearchResultsContainer = function(body) {
return body.find(SELECTORS.SEARCH_RESULTS_CONTAINER);
};
/**
* Get the search contacts container.
*
* @param {Object} body Search body container element.
* @return {Object} Search contacts container.
*/
var getContactsContainer = function(body) {
return body.find(SELECTORS.CONTACTS_CONTAINER);
};
/**
* Get the search non contacts container.
*
* @param {Object} body Search body container element.
* @return {Object} Search non contacts container.
*/
var getNonContactsContainer = function(body) {
return body.find(SELECTORS.NON_CONTACTS_CONTAINER);
};
/**
* Get the search messages container.
*
* @param {Object} body Search body container element.
* @return {Object} Search messages container.
*/
var getMessagesContainer = function(body) {
return body.find(SELECTORS.MESSAGES_CONTAINER);
};
/**
* Show the messages empty container.
*
* @param {Object} body Search body container element.
*/
var showEmptyMessage = function(body) {
getEmptyMessageContainer(body).removeClass('hidden');
};
/**
* Hide the messages empty container.
*
* @param {Object} body Search body container element.
*/
var hideEmptyMessage = function(body) {
getEmptyMessageContainer(body).addClass('hidden');
};
/**
* Show the loading icon.
*
* @param {Object} header Search header container element.
*/
var showLoadingIcon = function(header) {
getLoadingIconContainer(header).removeClass('hidden');
};
/**
* Hide the loading icon.
*
* @param {Object} header Search header container element.
*/
var hideLoadingIcon = function(header) {
getLoadingIconContainer(header).addClass('hidden');
};
/**
* Show loading placeholder.
*
* @param {Object} body Search body container element.
*/
var showLoadingPlaceholder = function(body) {
getLoadingPlaceholder(body).removeClass('hidden');
};
/**
* Hide loading placeholder.
*
* @param {Object} body Search body container element.
*/
var hideLoadingPlaceholder = function(body) {
getLoadingPlaceholder(body).addClass('hidden');
};
/**
* Show search icon.
*
* @param {Object} header Search header container element.
*/
var showSearchIcon = function(header) {
getSearchIconContainer(header).removeClass('hidden');
};
/**
* Hide search icon.
*
* @param {Object} header Search header container element.
*/
var hideSearchIcon = function(header) {
getSearchIconContainer(header).addClass('hidden');
};
/**
* Show search results.
*
* @param {Object} body Search body container element.
*/
var showSearchResults = function(body) {
getSearchResultsContainer(body).removeClass('hidden');
};
/**
* Hide search results.
*
* @param {Object} body Search body container element.
*/
var hideSearchResults = function(body) {
getSearchResultsContainer(body).addClass('hidden');
};
/**
* Disable the search input.
*
* @param {Object} header Search header container element.
*/
var disableSearchInput = function(header) {
getSearchInput(header).prop('disabled', true);
};
/**
* Enable the search input.
*
* @param {Object} header Search header container element.
*/
var enableSearchInput = function(header) {
getSearchInput(header).prop('disabled', false);
};
/**
* Clear the search input.
*
* @param {Object} header Search header container element.
*/
var clearSearchInput = function(header) {
getSearchInput(header).val('');
};
/**
* Clear all search results
*
* @param {Object} body Search body container element.
*/
var clearAllSearchResults = function(body) {
body.find(SELECTORS.CONTACTS_LIST).empty();
body.find(SELECTORS.NON_CONTACTS_LIST).empty();
body.find(SELECTORS.MESSAGES_LIST).empty();
body.find(SELECTORS.NO_RESULTS_CONTAINTER).addClass('hidden');
showLoadMoreUsersButton(body);
showLoadMoreMessagesButton(body);
};
/**
* Update the body and header to indicate the search is loading.
*
* @param {Object} header Search header container element.
* @param {Object} body Search body container element.
*/
var startLoading = function(header, body) {
hideSearchIcon(header);
hideEmptyMessage(body);
hideSearchResults(body);
showLoadingIcon(header);
showLoadingPlaceholder(body);
disableSearchInput(header);
};
/**
* Update the body and header to indicate the search has stopped loading.
*
* @param {Object} header Search header container element.
* @param {Object} body Search body container element.
*/
var stopLoading = function(header, body) {
showSearchIcon(header);
hideEmptyMessage(body);
showSearchResults(body);
hideLoadingIcon(header);
hideLoadingPlaceholder(body);
enableSearchInput(header);
};
/**
* Show the more users loading icon.
*
* @param {Object} root The more users container element.
*/
var showUsersLoadingIcon = function(root) {
var button = root.find(SELECTORS.LOAD_MORE_USERS);
button.prop('disabled', true);
button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
};
/**
* Hide the more users loading icon.
*
* @param {Object} root The more users container element.
*/
var hideUsersLoadingIcon = function(root) {
var button = root.find(SELECTORS.LOAD_MORE_USERS);
button.prop('disabled', false);
button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
};
/**
* Show the load more users button.
*
* @param {Object} root The users container element.
*/
var showLoadMoreUsersButton = function(root) {
root.find(SELECTORS.LOAD_MORE_USERS).removeClass('hidden');
};
/**
* Hide the load more users button.
*
* @param {Object} root The users container element.
*/
var hideLoadMoreUsersButton = function(root) {
root.find(SELECTORS.LOAD_MORE_USERS).addClass('hidden');
};
/**
* Show the messages are loading icon.
*
* @param {Object} root Messages root element.
*/
var showMessagesLoadingIcon = function(root) {
var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
button.prop('disabled', true);
button.find(SELECTORS.BUTTON_TEXT).addClass('hidden');
button.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
};
/**
* Hide the messages are loading icon.
*
* @param {Object} root Messages root element.
*/
var hideMessagesLoadingIcon = function(root) {
var button = root.find(SELECTORS.LOAD_MORE_MESSAGES);
button.prop('disabled', false);
button.find(SELECTORS.BUTTON_TEXT).removeClass('hidden');
button.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
};
/**
* Show the load more messages button.
*
* @param {Object} root The messages container element.
*/
var showLoadMoreMessagesButton = function(root) {
root.find(SELECTORS.LOAD_MORE_MESSAGES).removeClass('hidden');
};
/**
* Hide the load more messages button.
*
* @param {Object} root The messages container element.
*/
var hideLoadMoreMessagesButton = function(root) {
root.find(SELECTORS.LOAD_MORE_MESSAGES).addClass('hidden');
};
/**
* Find a contact in the search results.
*
* @param {Object} root Search results container element.
* @param {Number} userId User id.
* @return {Object} User container element.
*/
var findContact = function(root, userId) {
return root.find('[data-contact-user-id="' + userId + '"]');
};
/**
* Add a contact to the search results.
*
* @param {Object} root Search results container.
* @param {Object} contact User in contacts list.
*/
var addContact = function(root, contact) {
var nonContactsContainer = getNonContactsContainer(root);
var nonContact = findContact(nonContactsContainer, contact.userid);
if (nonContact.length) {
nonContact.remove();
var contactsContainer = getContactsContainer(root);
contactsContainer.removeClass('hidden');
contactsContainer.find(SELECTORS.LIST).append(nonContact);
}
if (!nonContactsContainer.find(SELECTORS.LIST).children().length) {
nonContactsContainer.addClass('hidden');
}
};
/**
* Remove a contact from the contacts results.
*
* @param {Object} root Search results container.
* @param {Object} userId Contact user id.
*/
var removeContact = function(root, userId) {
var contactsContainer = getContactsContainer(root);
var contact = findContact(contactsContainer, userId);
if (contact.length) {
contact.remove();
var nonContactsContainer = getNonContactsContainer(root);
nonContactsContainer.removeClass('hidden');
nonContactsContainer.find(SELECTORS.LIST).append(contact);
}
if (!contactsContainer.find(SELECTORS.LIST).children().length) {
contactsContainer.addClass('hidden');
}
};
/**
* Show the contact is blocked icon.
*
* @param {Object} root Search results container.
* @param {Object} userId Contact user id.
*/
var blockContact = function(root, userId) {
var contact = findContact(root, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).removeClass('hidden');
}
};
/**
* Hide the contact is blocked icon.
*
* @param {Object} root Search results container.
* @param {Object} userId Contact user id.
*/
var unblockContact = function(root, userId) {
var contact = findContact(root, userId);
if (contact.length) {
contact.find(SELECTORS.BLOCK_ICON_CONTAINER).addClass('hidden');
}
};
/**
* Render contacts in the contacts search results.
*
* @param {Object} root Search results container.
* @param {Array} contacts List of contacts.
* @return {Promise} Renderer promise.
*/
var renderContacts = function(root, contacts) {
var container = getContactsContainer(root);
var list = container.find(SELECTORS.LIST);
if (!contacts.length && !list.children().length) {
var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
noResultsContainer.removeClass('hidden');
return $.Deferred().resolve('').promise();
} else {
return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts})
.then(function(html) {
list.append(html);
return html;
});
}
};
/**
* Render non contacts in the contacts search results.
*
* @param {Object} root Search results container.
* @param {Array} nonContacts List of non contacts.
* @return {Promise} Renderer promise.
*/
var renderNonContacts = function(root, nonContacts) {
var container = getNonContactsContainer(root);
var list = container.find(SELECTORS.LIST);
if (!nonContacts.length && !list.children().length) {
var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
noResultsContainer.removeClass('hidden');
return $.Deferred().resolve('').promise();
} else {
return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts})
.then(function(html) {
list.append(html);
return html;
});
}
};
/**
* Render messages in the messages search results.
*
* @param {Object} root Search results container.
* @param {Array} messages List of messages.
* @return {Promise} Renderer promise.
*/
var renderMessages = function(root, messages) {
var container = getMessagesContainer(root);
var list = container.find(SELECTORS.LIST);
if (!messages.length && !list.children().length) {
var noResultsContainer = container.find(SELECTORS.NO_RESULTS_CONTAINTER);
noResultsContainer.removeClass('hidden');
return $.Deferred().resolve('').promise();
} else {
return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages})
.then(function(html) {
list.append(html);
return html;
});
}
};
/**
* Load more users from the repository and render the results into the users search results.
*
* @param {Object} root Search results container.
* @param {Number} loggedInUserId Current logged in user.
* @param {String} text Search text.
* @param {Number} limit Number of users to get.
* @param {Number} offset Load users from
* @return {Object} jQuery promise
*/
var loadMoreUsers = function(root, loggedInUserId, text, limit, offset) {
var loadedAll = false;
showUsersLoadingIcon(root);
return Repository.searchUsers(loggedInUserId, text, limit + 1, offset)
.then(function(results) {
var contacts = results.contacts;
var noncontacts = results.noncontacts;
if (contacts.length <= limit && noncontacts.length <= limit) {
loadedAll = true;
return {
contacts: contacts,
noncontacts: noncontacts
};
} else {
return {
contacts: contacts.slice(0, limit),
noncontacts: noncontacts.slice(0, limit)
};
}
})
.then(function(results) {
return $.when(
renderContacts(root, results.contacts),
renderNonContacts(root, results.noncontacts)
);
})
.then(function() {
hideUsersLoadingIcon(root);
if (loadedAll) {
hideLoadMoreUsersButton(root);
}
return;
})
.catch(function(error) {
hideUsersLoadingIcon(root);
// Rethrow error for other handlers.
throw error;
});
};
/**
* Load more messages from the repository and render the results into the messages search results.
*
* @param {Object} root Search results container.
* @param {Number} loggedInUserId Current logged in user.
* @param {String} text Search text.
* @param {Number} limit Number of messages to get.
* @param {Number} offset Load messages from
* @return {Object} jQuery promise
*/
var loadMoreMessages = function(root, loggedInUserId, text, limit, offset) {
var loadedAll = false;
showMessagesLoadingIcon(root);
return Repository.searchMessages(loggedInUserId, text, limit + 1, offset)
.then(function(results) {
var messages = results.contacts;
if (messages.length <= limit) {
loadedAll = true;
return messages;
} else {
return messages.slice(0, limit);
}
})
.then(function(messages) {
return renderMessages(root, messages);
})
.then(function() {
hideMessagesLoadingIcon(root);
if (loadedAll) {
hideLoadMoreMessagesButton(root);
}
return;
})
.catch(function(error) {
hideMessagesLoadingIcon(root);
// Rethrow error for other handlers.
throw error;
});
};
/**
* Search for users and messages.
*
* @param {Object} header Search header container element.
* @param {Object} body Search body container element.
* @param {String} searchText Search text.
* @param {Number} usersLimit The users limit.
* @param {Number} usersOffset The users offset.
* @param {Number} messagesLimit The message limit.
* @param {Number} messagesOffset The message offset.
* @return {Object} jQuery promise
*/
var search = function(header, body, searchText, usersLimit, usersOffset, messagesLimit, messagesOffset) {
var loggedInUserId = getLoggedInUserId(body);
startLoading(header, body);
clearAllSearchResults(body);
return $.when(
loadMoreUsers(body, loggedInUserId, searchText, usersLimit, usersOffset),
loadMoreMessages(body, loggedInUserId, searchText, messagesLimit, messagesOffset)
)
.then(function() {
stopLoading(header, body);
return;
});
};
/**
* Listen to and handle events for searching.
*
* @param {Object} header Search header container element.
* @param {Object} body Search body container element.
*/
var registerEventListeners = function(header, body) {
var loggedInUserId = getLoggedInUserId(body);
var searchInput = getSearchInput(header);
var searchText = '';
var messagesOffset = 0;
var usersOffset = 0;
var searchEventHandler = function(e, data) {
searchText = searchInput.val().trim();
if (searchText !== '') {
messagesOffset = 0;
usersOffset = 0;
search(
header,
body,
searchText,
USERS_INITIAL_SEARCH_LIMIT,
usersOffset,
MESSAGE_SEARCH_LIMIT,
messagesOffset
)
.then(function() {
searchInput.focus();
usersOffset = usersOffset + USERS_INITIAL_SEARCH_LIMIT;
messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
return;
})
.catch(Notification.exception);
}
data.originalEvent.preventDefault();
};
CustomEvents.define(searchInput, [CustomEvents.events.enter]);
CustomEvents.define(header, [CustomEvents.events.activate]);
CustomEvents.define(body, [CustomEvents.events.activate]);
searchInput.on(CustomEvents.events.enter, searchEventHandler);
header.on(CustomEvents.events.activate, SELECTORS.SEARCH_ACTION, searchEventHandler);
body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_MESSAGES, function(e, data) {
if (searchText !== '') {
loadMoreMessages(body, loggedInUserId, searchText, MESSAGE_SEARCH_LIMIT, messagesOffset)
.then(function() {
messagesOffset = messagesOffset + MESSAGE_SEARCH_LIMIT;
return;
})
.catch(Notification.exception);
}
data.originalEvent.preventDefault();
});
body.on(CustomEvents.events.activate, SELECTORS.LOAD_MORE_USERS, function(e, data) {
if (searchText !== '') {
loadMoreUsers(body, loggedInUserId, searchText, USERS_SEARCH_LIMIT, usersOffset)
.then(function() {
usersOffset = usersOffset + USERS_SEARCH_LIMIT;
return;
})
.catch(Notification.exception);
}
data.originalEvent.preventDefault();
});
header.on(CustomEvents.events.activate, SELECTORS.CANCEL_SEARCH_BUTTON, function() {
clearSearchInput(header);
showEmptyMessage(body);
showSearchIcon(header);
hideSearchResults(body);
hideLoadingIcon(header);
hideLoadingPlaceholder(body);
usersOffset = 0;
messagesOffset = 0;
});
PubSub.subscribe(Events.CONTACT_ADDED, function(userId) {
addContact(body, userId);
});
PubSub.subscribe(Events.CONTACT_REMOVED, function(userId) {
removeContact(body, userId);
});
PubSub.subscribe(Events.CONTACT_BLOCKED, function(userId) {
blockContact(body, userId);
});
PubSub.subscribe(Events.CONTACT_UNBLOCKED, function(userId) {
unblockContact(body, userId);
});
};
/**
* Setup the search page.
*
* @param {Object} header Contacts header container element.
* @param {Object} body Contacts body container element.
* @return {Object} jQuery promise
*/
var show = function(header, body) {
if (!body.attr('data-init')) {
registerEventListeners(header, body);
body.attr('data-init', true);
}
var searchInput = getSearchInput(header);
searchInput.focus();
return $.Deferred().resolve().promise();
};
/**
* String describing this page used for aria-labels.
*
* @param {Object} header Contacts header container element.
* @return {Object} jQuery promise
*/
var description = function(header) {
var searchInput = getSearchInput(header);
var searchText = searchInput.val().trim();
return Str.get_string('messagedrawerviewsearch', 'core_message', searchText);
};
return {
show: show,
description: description
};
});
@@ -0,0 +1,173 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the settings page in the message drawer.
*
* @module core_message/message_drawer_view_settings
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/str',
'core/pubsub',
'core_message/message_repository',
'core/custom_interaction_events',
'core_message/message_drawer_events'
],
function(
$,
Notification,
Str,
PubSub,
Repository,
CustomEvents,
MessageDrawerEvents
) {
var SELECTORS = {
SETTINGS: '[data-region="settings"]',
PREFERENCE_CONTROL: '[data-region="preference-control"]',
PRIVACY_PREFERENCE: '[data-preference="blocknoncontacts"] input[type="radio"]',
EMAIL_ENABLED_PREFERENCE: '[data-preference="emailnotifications"] input[type="checkbox"]',
ENTER_TO_SEND_PREFERENCE: '[data-preference="entertosend"] input[type="checkbox"]',
};
var PREFERENCES_EMAIL = {
'message_provider_moodle_instantmessage_loggedoff': {
type: 'emailnotifications',
enabled: 'email',
disabled: 'none'
},
'message_provider_moodle_instantmessage_loggedin': {
type: 'emailnotifications',
enabled: 'email',
disabled: 'none'
}
};
/**
* Create all of the event listeners for the message preferences page.
*
* @method registerEventListeners
* @param {Object} body The settings body element.
* @param {Number} loggedInUserId The logged in user id.
*/
var registerEventListeners = function(body, loggedInUserId) {
var settingsContainer = body.find(SELECTORS.SETTINGS);
CustomEvents.define(settingsContainer, [
CustomEvents.events.activate
]);
settingsContainer.on(CustomEvents.events.activate, SELECTORS.EMAIL_ENABLED_PREFERENCE, function(e) {
var checkbox = $(e.target);
var setting = checkbox.closest(SELECTORS.PREFERENCE_CONTROL);
var type = setting.attr('data-preference');
var isEnabled = checkbox.prop('checked');
var preferences = Object.keys(PREFERENCES_EMAIL).reduce(function(carry, preference) {
var config = PREFERENCES_EMAIL[preference];
if (config.type === type) {
carry.push({
type: preference,
value: isEnabled ? config.enabled : config.disabled
});
}
return carry;
}, []);
Repository.savePreferences(loggedInUserId, preferences)
.then(function() {
PubSub.publish(MessageDrawerEvents.PREFERENCES_UPDATED, preferences);
return;
})
.catch(Notification.exception);
}
);
settingsContainer.on(CustomEvents.events.activate, SELECTORS.PRIVACY_PREFERENCE, function(e) {
var newValue = $(e.target).val();
var preferences = [
{
type: 'message_blocknoncontacts',
value: newValue
}
];
Repository.savePreferences(loggedInUserId, preferences)
.then(function() {
PubSub.publish(MessageDrawerEvents.PREFERENCES_UPDATED, preferences);
return;
})
.catch(Notification.exception);
}
);
settingsContainer.on(CustomEvents.events.activate, SELECTORS.ENTER_TO_SEND_PREFERENCE, function(e) {
var newValue = $(e.target).prop('checked');
var preferences = [
{
type: 'message_entertosend',
value: newValue
}
];
Repository.savePreferences(loggedInUserId, preferences)
.then(function() {
PubSub.publish(MessageDrawerEvents.PREFERENCES_UPDATED, preferences);
return;
})
.catch(Notification.exception);
}
);
};
/**
* Initialise the settings page by adding event listeners to
* the checkboxes.
*
* @param {Object} header The settings header element.
* @param {Object} body The settings body element.
* @param {Number} loggedInUserId The logged in user id.
* @return {Object} jQuery promise
*/
var show = function(header, body, loggedInUserId) {
if (!body.attr('data-init')) {
registerEventListeners(body, loggedInUserId);
body.attr('data-init', true);
}
return $.Deferred().resolve().promise();
};
/**
* String describing this page used for aria-labels.
*
* @return {Object} jQuery promise
*/
var description = function() {
return Str.get_string('messagedrawerviewsettings', 'core_message');
};
return {
show: show,
description: description,
};
});
+102
View File
@@ -0,0 +1,102 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the message popover in the nav bar.
*
* @module core_message/message_popover
* @copyright 2018 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events',
'core/pubsub',
'core_message/message_drawer_events'
],
function(
$,
CustomEvents,
PubSub,
MessageDrawerEvents
) {
var SELECTORS = {
COUNT_CONTAINER: '[data-region="count-container"]'
};
/**
* Toggle the message drawer visibility.
*/
var toggleMessageDrawerVisibility = function() {
PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY);
};
/**
* Decrement the unread conversation count in the nav bar if a conversation
* is read. When there are no unread conversations then hide the counter.
*
* @param {Object} root The root element for the popover.
* @return {Function}
*/
var handleDecrementConversationCount = function(root) {
return function() {
var countContainer = root.find(SELECTORS.COUNT_CONTAINER);
var count = parseInt(countContainer.text(), 10);
if (isNaN(count)) {
countContainer.addClass('hidden');
} else if (!count || count < 2) {
countContainer.addClass('hidden');
} else {
count = count - 1;
countContainer.text(count);
}
};
};
/**
* Add events listeners for when the popover icon is clicked and when conversations
* are read.
*
* @param {Object} root The root element for the popover.
*/
var registerEventListeners = function(root) {
CustomEvents.define(root, [CustomEvents.events.activate]);
root.on(CustomEvents.events.activate, function(e, data) {
toggleMessageDrawerVisibility();
data.originalEvent.preventDefault();
});
PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, handleDecrementConversationCount(root));
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleDecrementConversationCount(root));
PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleDecrementConversationCount(root));
};
/**
* Initialise the message popover.
*
* @param {Object} root The root element for the popover.
*/
var init = function(root) {
root = $(root);
registerEventListeners(root);
};
return {
init: init,
};
});
+886
View File
@@ -23,6 +23,12 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
var CONVERSATION_TYPES = {
PRIVATE: 1,
PUBLIC: 2
};
/**
* Retrieve a list of messages from the server.
*
@@ -39,6 +45,14 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
args.offset = 0;
}
if (typeof args.type === 'undefined') {
args.type = null;
}
if (typeof args.favouritesonly === 'undefined') {
args.favouritesonly = false;
}
args.limitfrom = args.offset;
args.limitnum = args.limit;
@@ -96,9 +110,881 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
return promise;
};
/**
* Get contacts for given user.
*
* @param {int} userId The user id
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @return {object} jQuery promise
*/
var getContacts = function(userId, limit, offset) {
var args = {
userid: userId
};
if (typeof limit !== 'undefined') {
args.limitnum = limit;
}
if (typeof offset !== 'undefined') {
args.limitfrom = offset;
}
var request = {
methodname: 'core_message_data_for_messagearea_contacts',
args: args
};
return Ajax.call([request])[0];
};
/**
* Request profile information as a user for a given user.
*
* @param {int} userId The requesting user
* @param {int} profileUserId The id of the user who's profile is being requested
* @return {object} jQuery promise
*/
var getProfile = function(userId, profileUserId) {
var request = {
methodname: 'core_message_data_for_messagearea_get_profile',
args: {
currentuserid: userId,
otheruserid: profileUserId
}
};
return Ajax.call([request])[0];
};
/**
* Block a user.
*
* @param {int} userId The requesting user
* @param {int} blockedUserId Id of user to block
* @return {object} jQuery promise
*/
var blockUser = function(userId, blockedUserId) {
var requests = [
{
methodname: 'core_message_block_user',
args: {
userid: userId,
blockeduserid: blockedUserId
}
},
{
methodname: 'core_message_get_member_info',
args: {
referenceuserid: userId,
userids: [blockedUserId],
includecontactrequests: true,
includeprivacyinfo: true
}
}
];
// Wrap both requests in a single promise so that we can catch an error
// from either request.
return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) {
// Only return the profile.
return profiles.length ? profiles[0] : {};
});
};
/**
* Unblock a user.
*
* @param {int} userId The requesting user
* @param {int} unblockedUserId Id of user to unblock
* @return {object} jQuery promise
*/
var unblockUser = function(userId, unblockedUserId) {
var requests = [
{
methodname: 'core_message_unblock_user',
args: {
userid: userId,
unblockeduserid: unblockedUserId
}
},
{
methodname: 'core_message_get_member_info',
args: {
referenceuserid: userId,
userids: [unblockedUserId],
includecontactrequests: true,
includeprivacyinfo: true
}
}
];
// Wrap both requests in a single promise so that we can catch an error
// from either request.
return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) {
// Only return the profile.
return profiles.length ? profiles[0] : {};
});
};
/**
* Create a request to add a user as a contact.
*
* @param {int} userId The requesting user
* @param {int[]} requestUserIds List of user ids to add
* @return {object} jQuery promise
*/
var createContactRequest = function(userId, requestUserIds) {
var request = {
methodname: 'core_message_create_contact_request',
args: {
userid: userId,
requesteduserid: requestUserIds
}
};
return Ajax.call([request])[0];
};
/**
* Remove a list of users as contacts.
*
* @param {int} userId The requesting user
* @param {int[]} contactUserIds List of user ids to add
* @return {object} jQuery promise
*/
var deleteContacts = function(userId, contactUserIds) {
var requests = [
{
methodname: 'core_message_delete_contacts',
args: {
userid: userId,
userids: contactUserIds
}
},
{
methodname: 'core_message_get_member_info',
args: {
referenceuserid: userId,
userids: contactUserIds,
includecontactrequests: true,
includeprivacyinfo: true
}
}
];
return $.when.apply(null, Ajax.call(requests)).then(function(response1, profiles) {
// Return all of the profiles as an array.
return profiles;
});
};
/**
* Get messages between two users.
*
* @param {int} currentUserId The requesting user
* @param {int} conversationId Other user in the conversation
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @param {bool} newestFirst Order results by newest first
* @param {int} timeFrom Only return messages after this timestamp
* @return {object} jQuery promise
*/
var getMessages = function(currentUserId, conversationId, limit, offset, newestFirst, timeFrom) {
var args = {
currentuserid: currentUserId,
convid: conversationId,
newest: newestFirst ? true : false
};
if (typeof limit !== 'undefined') {
args.limitnum = limit;
}
if (typeof offset !== 'undefined') {
args.limitfrom = offset;
}
if (typeof timeFrom !== 'undefined') {
args.timefrom = timeFrom;
}
var request = {
methodname: 'core_message_get_conversation_messages',
args: args
};
return Ajax.call([request])[0];
};
/**
* Search for users.
*
* @param {int} userId The requesting user
* @param {string} searchString Search string
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @return {object} jQuery promise
*/
var searchUsers = function(userId, searchString, limit, offset) {
var args = {
userid: userId,
search: searchString
};
if (typeof limit !== 'undefined') {
args.limitnum = limit;
}
if (typeof offset !== 'undefined') {
args.limitfrom = offset;
}
var request = {
methodname: 'core_message_message_search_users',
args: args
};
return Ajax.call([request])[0];
};
/**
* Search for messages.
*
* @param {int} userId The requesting user
* @param {string} searchString Search string
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @return {object} jQuery promise
*/
var searchMessages = function(userId, searchString, limit, offset) {
var args = {
userid: userId,
search: searchString
};
if (typeof limit !== 'undefined') {
args.limitnum = limit;
}
if (typeof offset !== 'undefined') {
args.limitfrom = offset;
}
var request = {
methodname: 'core_message_data_for_messagearea_search_messages',
args: args
};
return Ajax.call([request])[0];
};
/**
* Send a list of messages to a user.
*
* @param {int} toUserId The recipient user id
* @param {string[]} messages List of messages to send
* @return {object} jQuery promise
*/
var sendMessagesToUser = function(toUserId, messages) {
var formattedMessages = messages.map(function(message) {
return {
touserid: toUserId,
text: message
};
});
var request = {
methodname: 'core_message_send_instant_messages',
args: {
messages: formattedMessages
}
};
return Ajax.call([request])[0]
.then(function(results) {
// Error handling for the weird way the old function works.
var errors = results.reduce(function(carry, result) {
if (result.errormessage) {
carry.push(result.errormessage);
}
return carry;
}, []);
if (errors.length) {
throw new Error(errors.join("\n"));
}
return results;
})
.then(function(results) {
// Format the results to match the other send message function.
return results.map(function(result) {
return {
id: result.msgid,
text: result.text,
timecreated: result.timecreated,
useridfrom: result.useridfrom,
conversationid: result.conversationid
};
});
});
};
/**
* Send a single message to a user.
*
* @param {int} toUserId The recipient user id
* @param {string} text The message text
* @return {object} jQuery promise
*/
var sendMessageToUser = function(toUserId, text) {
return sendMessagesToUser(toUserId, [text])
.then(function(results) {
return results[0];
});
};
/**
* Send messages to a conversation.
*
* @param {int} conversationId The conversation id
* @param {string[]} messages List of messages to send
* @return {object} jQuery promise
*/
var sendMessagesToConversation = function(conversationId, messages) {
var formattedMessages = messages.map(function(message) {
return {
text: message
};
});
var request = {
methodname: 'core_message_send_messages_to_conversation',
args: {
conversationid: conversationId,
messages: formattedMessages
}
};
return Ajax.call([request])[0];
};
/**
* Send a message to a conversation.
*
* @param {int} conversationId The conversation id
* @param {string} text The message text
* @return {object} jQuery promise
*/
var sendMessageToConversation = function(conversationId, text) {
return sendMessagesToConversation(conversationId, [text])
.then(function(result) {
return result[0];
});
};
/**
* Save message preferences.
*
* @param {int} userId The owner of the preferences
* @param {object[]} preferences New preferences values
* @return {object} jQuery promise
*/
var savePreferences = function(userId, preferences) {
var request = {
methodname: 'core_user_update_user_preferences',
args: {
userid: userId,
preferences: preferences
}
};
return Ajax.call([request])[0];
};
/**
* Get the user's preferences.
*
* @param {int} userId The target user
* @return {object} jQuery promise
*/
var getPreferences = function(userId) {
var request = {
methodname: 'core_user_get_user_preferences',
args: {
userid: userId
}
};
return Ajax.call([request])[0];
};
/**
* Delete a list of messages.
*
* @param {int} userId The user to delete messages for
* @param {int[]} messageIds List of message ids to delete
* @return {object} jQuery promise
*/
var deleteMessages = function(userId, messageIds) {
return Ajax.call(messageIds.map(function(messageId) {
return {
methodname: 'core_message_delete_message',
args: {
messageid: messageId,
userid: userId
}
};
}));
};
/**
* Delete a conversation between two users.
*
* @param {int} userId The user to delete messages for
* @param {int} otherUserId The other member of the conversation
* @return {object} jQuery promise
*/
var deleteCoversation = function(userId, otherUserId) {
var request = {
methodname: 'core_message_delete_conversation',
args: {
userid: userId,
otheruserid: otherUserId
}
};
return Ajax.call([request])[0];
};
/**
* Get the list of contact requests for a user.
*
* @param {int} userId The user id
* @return {object} jQuery promise
*/
var getContactRequests = function(userId) {
var request = {
methodname: 'core_message_get_contact_requests',
args: {
userid: userId
}
};
return Ajax.call([request])[0];
};
/**
* Accept a contact request.
*
* @param {int} sendingUserId The user that sent the request
* @param {int} recipientUserId The user that received the request
* @return {object} jQuery promise
*/
var acceptContactRequest = function(sendingUserId, recipientUserId) {
var requests = [
{
methodname: 'core_message_confirm_contact_request',
args: {
userid: sendingUserId,
requesteduserid: recipientUserId
}
},
{
methodname: 'core_message_get_member_info',
args: {
referenceuserid: recipientUserId,
userids: [sendingUserId],
includecontactrequests: true,
includeprivacyinfo: true
}
}
];
// Wrap both requests in a single promise so that we can catch an error
// from either request.
return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) {
// Only return the profile.
return profiles.length ? profiles[0] : {};
});
};
/**
* Decline a contact request.
*
* @param {int} sendingUserId The user that sent the request
* @param {int} recipientUserId The user that received the request
* @return {object} jQuery promise
*/
var declineContactRequest = function(sendingUserId, recipientUserId) {
var requests = [
{
methodname: 'core_message_decline_contact_request',
args: {
userid: sendingUserId,
requesteduserid: recipientUserId
}
},
{
methodname: 'core_message_get_member_info',
args: {
referenceuserid: recipientUserId,
userids: [sendingUserId],
includecontactrequests: true,
includeprivacyinfo: true
}
}
];
// Wrap both requests in a single promise so that we can catch an error
// from either request.
return $.when.apply(null, Ajax.call(requests)).then(function(reponse1, profiles) {
// Only return the profile.
return profiles.length ? profiles[0] : {};
});
};
/**
* Get a conversation.
*
* @param {int} loggedInUserId The logged in user
* @param {int} conversationId The conversation id
* @param {bool} includeContactRequests Incldue contact requests between members
* @param {bool} includePrivacyInfo Include privacy info for members
* @param {int} memberLimit Limit for members
* @param {int} memberOffset Offset for members
* @param {int} messageLimit Limit for messages
* @param {int} messageOffset Offset for messages
* @param {bool} newestMessagesFirst Order the messages by newest first
* @return {object} jQuery promise
*/
var getConversation = function(
loggedInUserId,
conversationId,
includeContactRequests,
includePrivacyInfo,
memberLimit,
memberOffset,
messageLimit,
messageOffset,
newestMessagesFirst
) {
var args = {
userid: loggedInUserId,
conversationid: conversationId
};
if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) {
args.includecontactrequests = includeContactRequests;
}
if (typeof includePrivacyInfo != 'undefined' && includePrivacyInfo !== null) {
args.includeprivacyinfo = includePrivacyInfo;
}
if (typeof memberLimit != 'undefined' && memberLimit !== null) {
args.memberlimit = memberLimit;
}
if (typeof memberOffset != 'undefined' && memberOffset !== null) {
args.memberoffset = memberOffset;
}
if (typeof messageLimit != 'undefined' && messageLimit !== null) {
args.messagelimit = messageLimit;
}
if (typeof messageOffset != 'undefined' && messageOffset !== null) {
args.messageoffset = messageOffset;
}
if (typeof newestMessagesFirst != 'undefined' && newestMessagesFirst !== null) {
args.newestmessagesfirst = newestMessagesFirst;
}
var request = {
methodname: 'core_message_get_conversation',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get a conversation between users.
*
* @param {int} loggedInUserId The logged in user
* @param {int} otherUserId The other user id
* @param {bool} includeContactRequests Incldue contact requests between members
* @param {bool} includePrivacyInfo Include privacy info for members
* @param {int} memberLimit Limit for members
* @param {int} memberOffset Offset for members
* @param {int} messageLimit Limit for messages
* @param {int} messageOffset Offset for messages
* @param {bool} newestMessagesFirst Order the messages by newest first
* @return {object} jQuery promise
*/
var getConversationBetweenUsers = function(
loggedInUserId,
otherUserId,
includeContactRequests,
includePrivacyInfo,
memberLimit,
memberOffset,
messageLimit,
messageOffset,
newestMessagesFirst
) {
var args = {
userid: loggedInUserId,
otheruserid: otherUserId
};
if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) {
args.includecontactrequests = includeContactRequests;
}
if (typeof includePrivacyInfo != 'undefined' && includePrivacyInfo !== null) {
args.includeprivacyinfo = includePrivacyInfo;
}
if (typeof memberLimit != 'undefined' && memberLimit !== null) {
args.memberlimit = memberLimit;
}
if (typeof memberOffset != 'undefined' && memberOffset !== null) {
args.memberoffset = memberOffset;
}
if (typeof messageLimit != 'undefined' && messageLimit !== null) {
args.messagelimit = messageLimit;
}
if (typeof messageOffset != 'undefined' && messageOffset !== null) {
args.messageoffset = messageOffset;
}
if (typeof newestMessagesFirst != 'undefined' && newestMessagesFirst !== null) {
args.newestmessagesfirst = newestMessagesFirst;
}
var request = {
methodname: 'core_message_get_conversation_between_users',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get the conversations for a user.
*
* @param {int} userId The logged in user
* @param {int|null} type The type of conversation to get
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @param {bool|null} favourites If favourites should be included or not
* @return {object} jQuery promise
*/
var getConversations = function(
userId,
type,
limit,
offset,
favourites
) {
var args = {
userid: userId,
type: type
};
if (typeof limit != 'undefined' && limit !== null) {
args.limitnum = limit;
}
if (typeof offset != 'undefined' && offset !== null) {
args.limitfrom = offset;
}
if (typeof favourites != 'undefined' && favourites !== null) {
args.favourites = favourites;
}
var request = {
methodname: 'core_message_get_conversations',
args: args
};
return Ajax.call([request])[0]
.then(function(result) {
if (result.conversations.length) {
result.conversations = result.conversations.map(function(conversation) {
if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
var otherUser = conversation.members.length ? conversation.members[0] : null;
if (otherUser) {
conversation.name = conversation.name ? conversation.name : otherUser.fullname;
conversation.imageurl = conversation.imageurl ? conversation.imageurl : otherUser.profileimageurl;
}
}
return conversation;
});
}
return result;
});
};
/**
* Get the conversations for a user.
*
* @param {int} conversationId The conversation id
* @param {int} loggedInUserId The logged in user
* @param {int} limit Limit for results
* @param {int} offset Offset for results
* @param {bool} includeContactRequests If contact requests should be included in result
* @return {object} jQuery promise
*/
var getConversationMembers = function(conversationId, loggedInUserId, limit, offset, includeContactRequests) {
var args = {
userid: loggedInUserId,
conversationid: conversationId
};
if (typeof limit != 'undefined' && limit !== null) {
args.limitnum = limit;
}
if (typeof offset != 'undefined' && offset !== null) {
args.limitfrom = offset;
}
if (typeof includeContactRequests != 'undefined' && includeContactRequests !== null) {
args.includecontactrequests = includeContactRequests;
}
var request = {
methodname: 'core_message_get_conversation_members',
args: args
};
return Ajax.call([request])[0];
};
/**
* Set a list of conversations to set as favourites for the given user.
*
* @param {int} userId The user id
* @param {array} conversationIds List of conversation ids to set as favourite
* @return {object} jQuery promise
*/
var setFavouriteConversations = function(userId, conversationIds) {
var request = {
methodname: 'core_message_set_favourite_conversations',
args: {
userid: userId,
conversations: conversationIds
}
};
return Ajax.call([request])[0];
};
/**
* Set a list of conversations to unset as favourites for the given user.
*
* @param {int} userId The user id
* @param {array} conversationIds List of conversation ids to unset as favourite
* @return {object} jQuery promise
*/
var unsetFavouriteConversations = function(userId, conversationIds) {
var request = {
methodname: 'core_message_unset_favourite_conversations',
args: {
userid: userId,
conversations: conversationIds
}
};
return Ajax.call([request])[0];
};
/**
* Get a list of user's member info.
*
* @param {int} referenceUserId The user id
* @param {array} userIds List of user ids to get
* @param {bool} includeContactRequests Include contact requests between users in response
* @param {bool} includePrivacyInfo Include privacy info for reference user in response
* @return {object} jQuery promise
*/
var getMemberInfo = function(referenceUserId, userIds, includeContactRequests, includePrivacyInfo) {
var args = {
referenceuserid: referenceUserId,
userids: userIds
};
if (typeof includeContactRequests != 'undefined') {
args.includecontactrequests = includeContactRequests;
}
if (typeof includePrivacyInfo != 'undefined') {
args.includeprivacyinfo = includePrivacyInfo;
}
var request = {
methodname: 'core_message_get_member_info',
args: args
};
return Ajax.call([request])[0];
};
/**
* Get a list of user's member info.
*
* @param {int} userId The user id to mark as read for
* @param {int} conversationId The conversation to mark as read
* @return {object} jQuery promise
*/
var markAllConversationMessagesAsRead = function(userId, conversationId) {
var request = {
methodname: 'core_message_mark_all_conversation_messages_as_read',
args: {
userid: userId,
conversationid: conversationId
}
};
return Ajax.call([request])[0];
};
return {
query: query,
countUnreadConversations: countUnreadConversations,
markAllAsRead: markAllAsRead,
getContacts: getContacts,
getProfile: getProfile,
blockUser: blockUser,
unblockUser: unblockUser,
createContactRequest: createContactRequest,
deleteContacts: deleteContacts,
getMessages: getMessages,
searchUsers: searchUsers,
searchMessages: searchMessages,
sendMessagesToUser: sendMessagesToUser,
sendMessageToUser: sendMessageToUser,
sendMessagesToConversation: sendMessagesToConversation,
sendMessageToConversation: sendMessageToConversation,
savePreferences: savePreferences,
getPreferences: getPreferences,
deleteMessages: deleteMessages,
deleteCoversation: deleteCoversation,
getContactRequests: getContactRequests,
acceptContactRequest: acceptContactRequest,
declineContactRequest: declineContactRequest,
getConversation: getConversation,
getConversationBetweenUsers: getConversationBetweenUsers,
getConversations: getConversations,
getConversationMembers: getConversationMembers,
setFavouriteConversations: setFavouriteConversations,
unsetFavouriteConversations: unsetFavouriteConversations,
getMemberInfo: getMemberInfo,
markAllConversationMessagesAsRead: markAllConversationMessagesAsRead
};
});
+305 -41
View File
@@ -801,6 +801,122 @@ class api {
return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
}
/**
* Return a conversation.
*
* @param int $userid The user id to get the conversation for
* @param int $conversationid The id of the conversation to fetch
* @param bool $includecontactrequests Should contact requests be included between members
* @param bool $includeprivacyinfo Should privacy info be included between members
* @param int $memberlimit Limit number of members to load
* @param int $memberoffset Offset members by this amount
* @param int $messagelimit Limit number of messages to load
* @param int $messageoffset Offset the messages
* @param bool $newestmessagesfirst Order messages by newest first
* @return \stdClass
*/
public static function get_conversation(
int $userid,
int $conversationid,
bool $includecontactrequests = false,
bool $includeprivacyinfo = false,
int $memberlimit = 0,
int $memberoffset = 0,
int $messagelimit = 0,
int $messageoffset = 0,
bool $newestmessagesfirst = true
) {
global $USER, $DB;
$systemcontext = \context_system::instance();
$canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
if (($USER->id != $userid) && !$canreadallmessages) {
throw new \moodle_exception('You do not have permission to perform this action.');
}
$conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
if (!$conversation) {
return null;
}
$isconversationmember = $DB->record_exists(
'message_conversation_members',
[
'conversationid' => $conversationid,
'userid' => $userid
]
);
if (!$isconversationmember && !$canreadallmessages) {
throw new \moodle_exception('You do not have permission to view this conversation.');
}
$members = self::get_conversation_members(
$userid,
$conversationid,
$includecontactrequests,
$includeprivacyinfo,
$memberoffset,
$memberlimit
);
// Strip out the requesting user to match what get_conversations does.
$members = array_filter($members, function($member) use ($userid) {
return $member->id != $userid;
});
$messages = self::get_conversation_messages(
$userid,
$conversationid,
$messageoffset,
$messagelimit,
$newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
);
$service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
$isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $systemcontext);
$convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
$subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
$imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
$unreadcountssql = 'SELECT count(m.id)
FROM {messages} m
INNER JOIN {message_conversations} mc
ON mc.id = m.conversationid
LEFT JOIN {message_user_actions} mua
ON (mua.messageid = m.id AND mua.userid = ? AND
(mua.action = ? OR mua.action = ?))
WHERE m.conversationid = ?
AND m.useridfrom != ?
AND mua.id is NULL';
$unreadcount = $DB->count_records_sql(
$unreadcountssql,
[
$userid,
self::MESSAGE_ACTION_READ,
self::MESSAGE_ACTION_DELETED,
$conversationid,
$userid
]
);
$membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
return (object) [
'id' => $conversation->id,
'name' => $conversation->name,
'subname' => $subname,
'imageurl' => $imageurl,
'type' => $conversation->type,
'membercount' => $membercount,
'isfavourite' => $isfavourite,
'isread' => empty($unreadcount),
'unreadcount' => $unreadcount,
'members' => $members,
'messages' => $messages['messages']
];
}
/**
* Mark a conversation as a favourite for the given user.
*
@@ -813,8 +929,13 @@ class api {
if (!self::is_user_in_conversation($userid, $conversationid)) {
throw new \moodle_exception("Conversation doesn't exist or user is not a member");
}
$systemcontext = \context_system::instance();
$ufservice = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, \context_system::instance());
if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $systemcontext)) {
return $favourite;
} else {
return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $systemcontext);
}
}
/**
@@ -881,6 +1002,21 @@ class api {
return [];
}
/**
* Returns the contacts count.
*
* @param int $userid The user id
* @return array
*/
public static function count_contacts(int $userid) : int {
global $DB;
$sql = "SELECT COUNT(id)
FROM {message_contacts}
WHERE userid = ? OR contactid = ?";
return $DB->count_records_sql($sql, [$userid, $userid]);
}
/**
* Returns the an array of the users the given user is in a conversation
* with who are a contact and the number of unread messages.
@@ -1031,12 +1167,8 @@ class api {
}
}
$arrmessages = array();
if ($messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto)) {
$arrmessages = helper::format_conversation_messages($userid, $convid, $messages);
}
return $arrmessages;
$messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
return helper::format_conversation_messages($userid, $convid, $messages);
}
/**
@@ -1296,6 +1428,84 @@ class api {
return false;
}
/**
* Returns the count of conversations (collection of messages from a single user) for
* the given user.
*
* @param \stdClass $user The user who's conversations should be counted
* @param int $type The conversation type
* @param bool $excludefavourites Exclude favourite conversations
* @return int the count of the user's unread conversations
*/
public static function count_conversations($user, int $type = null, bool $excludefavourites = false) {
global $DB;
$params = [];
$favouritessql = '';
if ($excludefavourites) {
$favouritessql = "AND m.conversationid NOT IN (
SELECT itemid
FROM {favourite}
WHERE component = 'core_message'
AND itemtype = 'message_conversations'
AND userid = ?
)";
$params[] = $user->id;
}
switch($type) {
case null:
$params = array_merge([$user->id, self::MESSAGE_ACTION_DELETED, $user->id], $params);
$sql = "SELECT COUNT(DISTINCT(m.conversationid))
FROM {messages} m
LEFT JOIN {message_conversations} c
ON m.conversationid = c.id
LEFT JOIN {message_user_actions} ma
ON ma.messageid = m.id
LEFT JOIN {message_conversation_members} mcm
ON m.conversationid = mcm.conversationid
WHERE mcm.userid = ?
AND (
c.type != " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . "
OR
(
(ma.action IS NULL OR ma.action != ? OR ma.userid != ?)
AND c.type = " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . "
)
)
${favouritessql}";
break;
case self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL:
$params = array_merge([self::MESSAGE_ACTION_DELETED, $user->id, $user->id], $params);
$sql = "SELECT COUNT(DISTINCT(m.conversationid))
FROM {messages} m
LEFT JOIN {message_conversations} c
ON m.conversationid = c.id
LEFT JOIN {message_user_actions} ma
ON ma.messageid = m.id
LEFT JOIN {message_conversation_members} mcm
ON m.conversationid = mcm.conversationid
WHERE (ma.action IS NULL OR ma.action != ? OR ma.userid != ?)
AND mcm.userid = ?
AND c.type = " . self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL . "
${favouritessql}";
break;
default:
$params = array_merge([$user->id, $type], $params);
$sql = "SELECT COUNT(m.conversationid)
FROM {message_conversation_members} m
LEFT JOIN {message_conversations} c
ON m.conversationid = c.id
WHERE m.userid = ?
AND c.type = ?
${favouritessql}";
}
return $DB->count_records_sql($sql, $params);
}
/**
* Marks all messages being sent to a user in a particular conversation.
*
@@ -1959,19 +2169,65 @@ class api {
public static function get_conversation_between_users(array $userids) {
global $DB;
$hash = helper::get_conversation_hash($userids);
$conversations = self::get_individual_conversations_between_users([$userids]);
$conversation = $conversations[0];
$params = [
'type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
'convhash' => $hash
];
if ($conversation = $DB->get_record('message_conversations', $params)) {
if ($conversation) {
return $conversation->id;
}
return false;
}
/**
* Returns the conversations between sets of users.
*
* The returned array of results will be in the same order as the requested
* arguments, null will be returned if there is no conversation for that user
* pair.
*
* For example:
* If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
* exist. One between 1 and 2 and another between 5 and 6.
*
* Then if we call:
* $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
*
* The conversations array will look like:
* [<conv_record>, null, <conv_record>];
*
* Where null is returned for the pairing of [3, 4] since no record exists.
*
* @param array $useridsets An array of arrays where the inner array is the set of user ids
* @return stdClass[] Array of conversation records
*/
public static function get_individual_conversations_between_users(array $useridsets) : array {
global $DB;
if (empty($useridsets)) {
return [];
}
$hashes = array_map(function($userids) {
return helper::get_conversation_hash($userids);
}, $useridsets);
list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
$where = "type = ? AND convhash ${inorequalsql}";
$conversations = array_fill(0, count($hashes), null);
$records = $DB->get_records_select('message_conversations', $where, $params);
foreach (array_values($records) as $record) {
$index = array_search($record->convhash, $hashes);
if ($index !== false) {
$conversations[$index] = $record;
}
}
return $conversations;
}
/**
* Creates a conversation between two users.
*
@@ -2105,8 +2361,9 @@ class api {
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return \stdClass the request
*/
public static function create_contact_request(int $userid, int $requesteduserid) {
public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
global $DB;
$request = new \stdClass();
@@ -2114,32 +2371,9 @@ class api {
$request->requesteduserid = $requesteduserid;
$request->timecreated = time();
$DB->insert_record('message_contact_requests', $request);
$request->id = $DB->insert_record('message_contact_requests', $request);
// Send a notification.
$userfrom = \core_user::get_user($userid);
$userfromfullname = fullname($userfrom);
$userto = \core_user::get_user($requesteduserid);
$url = new \moodle_url('/message/pendingcontactrequests.php');
$subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
$fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
$message = new \core\message\message();
$message->courseid = SITEID;
$message->component = 'moodle';
$message->name = 'messagecontactrequests';
$message->notification = 1;
$message->userfrom = $userfrom;
$message->userto = $userto;
$message->subject = $subject;
$message->fullmessage = text_to_html($fullmessage);
$message->fullmessageformat = FORMAT_HTML;
$message->fullmessagehtml = $fullmessage;
$message->smallmessage = '';
$message->contexturl = $url->out(false);
message_send($message);
return $request;
}
@@ -2206,6 +2440,17 @@ class api {
return [];
}
/**
* Count how many contact requests the user has received.
*
* @param \stdClass $user The user to fetch contact requests for
* @return int The count
*/
public static function count_received_contact_requests(\stdClass $user) : int {
global $DB;
return $DB->count_records('message_contact_requests', ['requesteduserid' => $user->id]);
}
/**
* Handles adding a contact.
*
@@ -2353,6 +2598,23 @@ class api {
return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
}
/**
* Get contact requests between users.
*
* @param int $userid The id of the user who is creating the contact request
* @param int $requesteduserid The id of the user being requested
* @return \stdClass[]
*/
public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
global $DB;
$sql = "SELECT *
FROM {message_contact_requests} mcr
WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
}
/**
* Checks if a contact request already exists between users.
*
@@ -2636,18 +2898,20 @@ class api {
* @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
* @param int $conversationid The id of the conversation
* @param bool $includecontactrequests Do we want to include contact requests with this data?
* @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
int $limitfrom = 0, int $limitnum = 0) : array {
bool $includeprivacyinfo = false, int $limitfrom = 0,
int $limitnum = 0) : array {
global $DB;
if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
$userids = array_keys($members);
$members = helper::get_member_info($userid, $userids, $includecontactrequests);
$members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
return $members;
}
+12 -6
View File
@@ -218,10 +218,13 @@ class helper {
$memberids = array_unique(array_map(function($message) {
return $message->useridfrom;
}, $messages));
// Get members information.
$arrmembers = self::get_member_info($userid, $memberids);
// Add the members to the conversation.
$conversation['members'] = $arrmembers;
if (!empty($memberids)) {
// Get members information.
$conversation['members'] = self::get_member_info($userid, $memberids);
} else {
$conversation['members'] = array();
}
return $conversation;
}
@@ -303,11 +306,13 @@ class helper {
// Store the message if we have it.
$data->ismessaging = false;
$data->lastmessage = null;
$data->lastmessagedate = null;
$data->messageid = null;
if (isset($contact->smallmessage)) {
$data->ismessaging = true;
// Strip the HTML tags from the message for displaying in the contact area.
$data->lastmessage = clean_param($contact->smallmessage, PARAM_NOTAGS);
$data->lastmessagedate = $contact->timecreated;
$data->useridfrom = $contact->useridfrom;
if (isset($contact->messageid)) {
$data->messageid = $contact->messageid;
@@ -504,11 +509,11 @@ class helper {
$userssql = "SELECT $userfields, u.deleted, mc.id AS contactid, mub.id AS blockedid
FROM {user} u
LEFT JOIN {message_contacts} mc
ON (mc.userid = ? AND mc.contactid = u.id)
ON ((mc.userid = ? AND mc.contactid = u.id) OR (mc.userid = u.id AND mc.contactid = ?))
LEFT JOIN {message_users_blocked} mub
ON (mub.userid = ? AND mub.blockeduserid = u.id)
WHERE u.id $useridsql";
$usersparams = array_merge([$referenceuserid, $referenceuserid], $usersparams);
$usersparams = array_merge([$referenceuserid, $referenceuserid, $referenceuserid], $usersparams);
$otherusers = $DB->get_records_sql($userssql, $usersparams);
$members = [];
@@ -605,6 +610,7 @@ class helper {
$data->profileimageurlsmall = $conv->members[$otheruser->id]->profileimageurlsmall;
$data->ismessaging = isset($conv->messages[0]->text) ? true : false;
$data->lastmessage = $conv->messages[0]->text ? clean_param($conv->messages[0]->text, PARAM_NOTAGS) : null;
$data->lastmessagedate = $conv->messages[0]->timecreated ?? null;
$data->messageid = $conv->messages[0]->id ?? null;
$data->isonline = $conv->members[$otheruser->id]->isonline ?? null;
$data->isblocked = $conv->members[$otheruser->id]->isblocked ?? null;
@@ -83,6 +83,11 @@ class contact implements templatable, renderable {
*/
public $lastmessage;
/**
* @var int The last message sent timestamp.
*/
public $lastmessagedate;
/**
* @var bool Is the user online?
*/
@@ -117,6 +122,7 @@ class contact implements templatable, renderable {
$this->messageid = $contact->messageid;
$this->ismessaging = $contact->ismessaging;
$this->lastmessage = $contact->lastmessage;
$this->lastmessagedate = $contact->lastmessagedate;
$this->isonline = $contact->isonline;
$this->isblocked = $contact->isblocked;
$this->isread = $contact->isread;
@@ -140,6 +146,7 @@ class contact implements templatable, renderable {
} else {
$contact->lastmessage = null;
}
$contact->lastmessagedate = $this->lastmessagedate;
$contact->showonlinestatus = is_null($this->isonline) ? false : true;
$contact->isonline = $this->isonline;
$contact->isblocked = $this->isblocked;
+5 -1
View File
@@ -158,7 +158,11 @@ class provider implements
public static function export_user_preferences(int $userid) {
$preferences = get_user_preferences(null, null, $userid);
foreach ($preferences as $name => $value) {
if ((substr($name, 0, 16) == 'message_provider') || ($name == 'message_blocknoncontacts')) {
if (
(substr($name, 0, 16) == 'message_provider') ||
($name == 'message_blocknoncontacts') ||
($name == 'message_entertosend')
) {
writer::export_user_preference(
'core_message',
$name,
+12 -5
View File
@@ -27,6 +27,7 @@ require_once($CFG->dirroot . '/message/lib.php');
require_once($CFG->dirroot . '/user/lib.php');
$userid = optional_param('id', 0, PARAM_INT); // User id.
$currentuser = true;
if (!$userid) {
$userid = $USER->id;
@@ -58,6 +59,7 @@ if ($user->id == $USER->id) {
//editing own message profile
require_capability('moodle/user:editownmessageprofile', $systemcontext);
} else {
$currentuser = false;
// teachers, parents, etc.
require_capability('moodle/user:editmessageprofile', $personalcontext);
// no editing of guest user account
@@ -77,11 +79,16 @@ $strmessaging = get_string('messagepreferences', 'message');
$PAGE->set_title($strmessaging);
$PAGE->set_heading(fullname($user));
// Grab the renderer
$renderer = $PAGE->get_renderer('core', 'message');
$messagingoptions = $renderer->render_user_message_preferences($user);
echo $OUTPUT->header();
echo $messagingoptions;
if ($currentuser) {
// Open the message drawer to show the settings.
echo $OUTPUT->heading(get_string('messagepreferences', 'core_message'));
$PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'showSettings');
} else {
// Viewing another user's preferences so render the old page.
$renderer = $PAGE->get_renderer('core', 'message');
echo $renderer->render_user_message_preferences($user);
}
echo $OUTPUT->footer();
+344 -16
View File
@@ -90,8 +90,13 @@ class core_message_external extends external_api {
$messages = [];
foreach ($params['messages'] as $message) {
$messages[] = \core_message\api::send_message_to_conversation($USER->id, $params['conversationid'], $message['text'],
$createdmessage = \core_message\api::send_message_to_conversation($USER->id, $params['conversationid'], $message['text'],
$message['textformat']);
$createdmessage->text = message_format_message_text((object) [
'smallmessage' => $createdmessage->text,
'fullmessageformat' => external_validate_format($message['textformat'])
]);
$messages[] = $createdmessage;
}
return $messages;
@@ -164,6 +169,7 @@ class core_message_external extends external_api {
$tousers = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams);
$resultmessages = array();
$messageids = array();
foreach ($params['messages'] as $message) {
$resultmsg = array(); //the infos about the success of the operation
@@ -197,6 +203,12 @@ class core_message_external extends external_api {
}
if ($success) {
$resultmsg['msgid'] = $success;
$resultmsg['text'] = message_format_message_text((object) [
'smallmessage' => $message['text'],
'fullmessageformat' => external_validate_format($message['textformat'])
]);
$resultmsg['timecreated'] = time();
$messageids[] = $success;
} else {
// WARNINGS: for backward compatibility we return this errormessage.
// We should have thrown exceptions as these errors prevent results to be returned.
@@ -208,6 +220,16 @@ class core_message_external extends external_api {
$resultmessages[] = $resultmsg;
}
if (!empty($messageids)) {
$messagerecords = $DB->get_records_list('messages', 'id', $messageids, '', 'id, conversationid');
$resultmessages = array_map(function($resultmessage) use ($messagerecords, $USER) {
$id = $resultmessage['msgid'];
$resultmessage['conversationid'] = isset($messagerecords[$id]) ? $messagerecords[$id]->conversationid : null;
$resultmessage['useridfrom'] = $USER->id;
return $resultmessage;
}, $resultmessages);
}
return $resultmessages;
}
@@ -223,7 +245,11 @@ class core_message_external extends external_api {
array(
'msgid' => new external_value(PARAM_INT, 'test this to know if it succeeds: id of the created message if it succeeded, -1 when failed'),
'clientmsgid' => new external_value(PARAM_ALPHANUMEXT, 'your own id for the message', VALUE_OPTIONAL),
'errormessage' => new external_value(PARAM_TEXT, 'error message - if it failed', VALUE_OPTIONAL)
'errormessage' => new external_value(PARAM_TEXT, 'error message - if it failed', VALUE_OPTIONAL),
'text' => new external_value(PARAM_RAW, 'The text of the message', VALUE_OPTIONAL),
'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message', VALUE_OPTIONAL),
'conversationid' => new external_value(PARAM_INT, 'The conversation id for this message', VALUE_OPTIONAL),
'useridfrom' => new external_value(PARAM_INT, 'The user id who sent the message', VALUE_OPTIONAL),
)
)
);
@@ -738,6 +764,8 @@ class core_message_external extends external_api {
'conversationid' => new external_value(PARAM_INT, 'The id of the conversation'),
'includecontactrequests' => new external_value(PARAM_BOOL, 'Do we want to include contact requests?',
VALUE_DEFAULT, false),
'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Do we want to include privacy info?',
VALUE_DEFAULT, false),
'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0)
]
@@ -750,12 +778,13 @@ class core_message_external extends external_api {
* @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
* @param int $conversationid The id of the conversation
* @param bool $includecontactrequests Do we want to include contact requests with this data?
* @param bool $includeprivacyinfo Do we want to include privacy info?
* @param int $limitfrom
* @param int $limitnum
* @return array
*/
public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
int $limitfrom = 0, int $limitnum = 0) {
bool $includeprivacyinfo = false, int $limitfrom = 0, int $limitnum = 0) {
global $CFG, $USER;
// Check if messaging is enabled.
@@ -771,6 +800,7 @@ class core_message_external extends external_api {
'userid' => $userid,
'conversationid' => $conversationid,
'includecontactrequests' => $includecontactrequests,
'includeprivacyinfo' => $includeprivacyinfo,
'limitfrom' => $limitfrom,
'limitnum' => $limitnum
];
@@ -786,9 +816,8 @@ class core_message_external extends external_api {
throw new moodle_exception('You are not a member of this conversation.');
}
return \core_message\api::get_conversation_members($params['userid'], $params['conversationid'], $params['includecontactrequests'],
$params['limitfrom'], $params['limitnum']);
$params['includeprivacyinfo'], $params['limitfrom'], $params['limitnum']);
}
/**
@@ -842,21 +871,27 @@ class core_message_external extends external_api {
throw new required_capability_exception($context, $capability, 'nopermissions', '');
}
$result = [
'warnings' => []
];
if (!\core_message\api::can_create_contact($params['userid'], $params['requesteduserid'])) {
$warning[] = [
$result['warnings'][] = [
'item' => 'user',
'itemid' => $params['requesteduserid'],
'warningcode' => 'cannotcreatecontactrequest',
'message' => 'You are unable to create a contact request for this user'
];
return $warning;
} else {
if ($requests = \core_message\api::get_contact_requests_between_users($params['userid'], $params['requesteduserid'])) {
// There should only ever be one but just in case there are multiple then we can return the first.
$result['request'] = array_shift($requests);
} else {
$result['request'] = \core_message\api::create_contact_request($params['userid'], $params['requesteduserid']);
}
}
if (!\core_message\api::does_contact_request_exist($params['userid'], $params['requesteduserid'])) {
\core_message\api::create_contact_request($params['userid'], $params['requesteduserid']);
}
return [];
return $result;
}
/**
@@ -865,7 +900,21 @@ class core_message_external extends external_api {
* @return external_description
*/
public static function create_contact_request_returns() {
return new external_warnings();
return new external_single_structure(
array(
'request' => new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'Message id'),
'userid' => new external_value(PARAM_INT, 'User from id'),
'requesteduserid' => new external_value(PARAM_INT, 'User to id'),
'timecreated' => new external_value(PARAM_INT, 'Time created'),
),
'request record',
VALUE_OPTIONAL
),
'warnings' => new external_warnings()
)
);
}
/**
@@ -992,6 +1041,7 @@ class core_message_external extends external_api {
'ismessaging' => new external_value(PARAM_BOOL, 'If we are messaging the user'),
'sentfromcurrentuser' => new external_value(PARAM_BOOL, 'Was the last message sent from the current user?'),
'lastmessage' => new external_value(PARAM_NOTAGS, 'The user\'s last message'),
'lastmessagedate' => new external_value(PARAM_INT, 'Timestamp for last message', VALUE_DEFAULT, null),
'messageid' => new external_value(PARAM_INT, 'The unique search message id', VALUE_DEFAULT, null),
'showonlinestatus' => new external_value(PARAM_BOOL, 'Show the user\'s online status?'),
'isonline' => new external_value(PARAM_BOOL, 'The user\'s online status'),
@@ -1009,7 +1059,6 @@ class core_message_external extends external_api {
* @return external_single_structure
* @since Moodle 3.6
*/
private static function get_conversation_structure() {
return new external_single_structure(
array(
@@ -1569,12 +1618,217 @@ class core_message_external extends external_api {
return new external_single_structure(
[
'conversations' => new external_multiple_structure(
self::get_conversation_structure()
self::get_conversation_structure(true)
)
]
);
}
/**
* Get conversation parameters.
*
* @return external_function_parameters
*/
public static function get_conversation_parameters() {
return new external_function_parameters(
array(
'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'),
'conversationid' => new external_value(PARAM_INT, 'The id of the conversation to fetch'),
'includecontactrequests' => new external_value(PARAM_BOOL, 'Include contact requests in the members'),
'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Include privacy info in the members'),
'memberlimit' => new external_value(PARAM_INT, 'Limit for number of members', VALUE_DEFAULT, 0),
'memberoffset' => new external_value(PARAM_INT, 'Offset for member list', VALUE_DEFAULT, 0),
'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100),
'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0),
'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true)
)
);
}
/**
* Get a single conversation.
*
* @param int $userid The user id to get the conversation for
* @param int $conversationid The id of the conversation to fetch
* @param bool $includecontactrequests Should contact requests be included between members
* @param bool $includeprivacyinfo Should privacy info be included between members
* @param int $memberlimit Limit number of members to load
* @param int $memberoffset Offset members by this amount
* @param int $messagelimit Limit number of messages to load
* @param int $messageoffset Offset the messages
* @param bool $newestmessagesfirst Order messages by newest first
* @return stdClass
* @throws \moodle_exception if the messaging feature is disabled on the site.
*/
public static function get_conversation(
int $userid,
int $conversationid,
bool $includecontactrequests = false,
bool $includeprivacyinfo = false,
int $memberlimit = 0,
int $memberoffset = 0,
int $messagelimit = 0,
int $messageoffset = 0,
bool $newestmessagesfirst = true
) {
global $CFG, $DB, $USER;
// All the standard BL checks.
if (empty($CFG->messaging)) {
throw new moodle_exception('disabled', 'message');
}
$params = [
'userid' => $userid,
'conversationid' => $conversationid,
'includecontactrequests' => $includecontactrequests,
'includeprivacyinfo' => $includeprivacyinfo,
'memberlimit' => $memberlimit,
'memberoffset' => $memberoffset,
'messagelimit' => $messagelimit,
'messageoffset' => $messageoffset,
'newestmessagesfirst' => $newestmessagesfirst
];
self::validate_parameters(self::get_conversation_parameters(), $params);
$systemcontext = context_system::instance();
self::validate_context($systemcontext);
$conversation = \core_message\api::get_conversation(
$params['userid'],
$params['conversationid'],
$params['includecontactrequests'],
$params['includeprivacyinfo'],
$params['memberlimit'],
$params['memberoffset'],
$params['messagelimit'],
$params['messageoffset'],
$params['newestmessagesfirst']
);
if ($conversation) {
return $conversation;
} else {
// We have to throw an exception here because the external functions annoyingly
// don't accept null to be returned for a single structure.
throw new \moodle_exception('Conversation does not exist');
}
}
/**
* Get conversation returns.
*
* @return external_single_structure
*/
public static function get_conversation_returns() {
return self::get_conversation_structure();
}
/**
* Get conversation parameters.
*
* @return external_function_parameters
*/
public static function get_conversation_between_users_parameters() {
return new external_function_parameters(
array(
'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing conversations for'),
'otheruserid' => new external_value(PARAM_INT, 'The other user id'),
'includecontactrequests' => new external_value(PARAM_BOOL, 'Include contact requests in the members'),
'includeprivacyinfo' => new external_value(PARAM_BOOL, 'Include privacy info in the members'),
'memberlimit' => new external_value(PARAM_INT, 'Limit for number of members', VALUE_DEFAULT, 0),
'memberoffset' => new external_value(PARAM_INT, 'Offset for member list', VALUE_DEFAULT, 0),
'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100),
'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0),
'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true)
)
);
}
/**
* Get a single conversation between users.
*
* @param int $userid The user id to get the conversation for
* @param int $otheruserid The other user id
* @param bool $includecontactrequests Should contact requests be included between members
* @param bool $includeprivacyinfo Should privacy info be included between members
* @param int $memberlimit Limit number of members to load
* @param int $memberoffset Offset members by this amount
* @param int $messagelimit Limit number of messages to load
* @param int $messageoffset Offset the messages
* @param bool $newestmessagesfirst Order messages by newest first
* @return stdClass
* @throws \moodle_exception if the messaging feature is disabled on the site.
*/
public static function get_conversation_between_users(
int $userid,
int $otheruserid,
bool $includecontactrequests = false,
bool $includeprivacyinfo = false,
int $memberlimit = 0,
int $memberoffset = 0,
int $messagelimit = 0,
int $messageoffset = 0,
bool $newestmessagesfirst = true
) {
global $CFG, $DB, $USER;
// All the standard BL checks.
if (empty($CFG->messaging)) {
throw new moodle_exception('disabled', 'message');
}
$params = [
'userid' => $userid,
'otheruserid' => $otheruserid,
'includecontactrequests' => $includecontactrequests,
'includeprivacyinfo' => $includeprivacyinfo,
'memberlimit' => $memberlimit,
'memberoffset' => $memberoffset,
'messagelimit' => $messagelimit,
'messageoffset' => $messageoffset,
'newestmessagesfirst' => $newestmessagesfirst
];
self::validate_parameters(self::get_conversation_between_users_parameters(), $params);
$systemcontext = context_system::instance();
self::validate_context($systemcontext);
$conversationid = \core_message\api::get_conversation_between_users([$params['userid'], $params['otheruserid']]);
$conversation = null;
if ($conversationid) {
$conversation = \core_message\api::get_conversation(
$params['userid'],
$conversationid,
$params['includecontactrequests'],
$params['includeprivacyinfo'],
$params['memberlimit'],
$params['memberoffset'],
$params['messagelimit'],
$params['messageoffset'],
$params['newestmessagesfirst']
);
}
if ($conversation) {
return $conversation;
} else {
// We have to throw an exception here because the external functions annoyingly
// don't accept null to be returned for a single structure.
throw new \moodle_exception('Conversation does not exist');
}
}
/**
* Get conversation returns.
*
* @return external_single_structure
*/
public static function get_conversation_between_users_returns() {
return self::get_conversation_structure(true);
}
/**
* The messagearea conversations parameters.
*
@@ -3196,7 +3450,7 @@ class core_message_external extends external_api {
* @since 3.6
*/
public static function mark_all_conversation_messages_as_read_returns() {
return new external_warnings();
return null;
}
/**
@@ -3953,4 +4207,78 @@ class core_message_external extends external_api {
public static function unset_favourite_conversations_returns() {
return new external_warnings();
}
/**
* Returns description of method parameters for get_member_info() method.
*
* @return external_function_parameters
*/
public static function get_member_info_parameters() {
return new external_function_parameters(
array(
'referenceuserid' => new external_value(PARAM_INT, 'id of the user'),
'userids' => new external_multiple_structure(
new external_value(PARAM_INT, 'id of members to get')
),
'includecontactrequests' => new external_value(PARAM_BOOL, 'include contact requests in response', VALUE_DEFAULT, false),
'includeprivacyinfo' => new external_value(PARAM_BOOL, 'include privacy info in response', VALUE_DEFAULT, false)
)
);
}
/**
* Returns conversation member info for the supplied users, relative to the supplied referenceuserid.
*
* This is the basic structure used when returning members, and includes information about the relationship between each member
* and the referenceuser, such as a whether the referenceuser has marked the member as a contact, or has blocked them.
*
* @param int $referenceuserid the id of the user which check contact and blocked status.
* @param array $userids
* @return array the array of objects containing member info.
* @throws moodle_exception if messaging is disabled or if the user cannot perform the action.
*/
public static function get_member_info(
int $referenceuserid,
array $userids,
bool $includecontactrequests = false,
bool $includeprivacyinfo = false
) {
global $CFG, $USER;
// All the business logic checks that really shouldn't be in here.
if (empty($CFG->messaging)) {
throw new moodle_exception('disabled', 'message');
}
$params = [
'referenceuserid' => $referenceuserid,
'userids' => $userids,
'includecontactrequests' => $includecontactrequests,
'includeprivacyinfo' => $includeprivacyinfo
];
$params = self::validate_parameters(self::get_member_info_parameters(), $params);
$systemcontext = context_system::instance();
self::validate_context($systemcontext);
if (($USER->id != $referenceuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
throw new moodle_exception('You do not have permission to perform this action.');
}
return \core_message\helper::get_member_info(
$params['referenceuserid'],
$params['userids'],
$params['includecontactrequests'],
$params['includeprivacyinfo']
);
}
/**
* Get member info return description.
*
* @return external_description
*/
public static function get_member_info_returns() {
return new external_multiple_structure(
self::get_conversation_member_structure(true)
);
}
}
+21
View File
@@ -99,6 +99,27 @@ $usernode->remove();
$settings = $PAGE->settingsnav->find('messages', null);
$settings->make_active();
if ($currentuser) {
// We're in the pprocess of deprecating this page however we haven't replaced the functionality
// for the admin (or user with correct capabilities) to view other user's conversations. For the
// time being this page will simply open the message drawer unless it's the admin user case just
// mentioned. In that case we will render the old UI for backwards compatibility.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('messages', 'message'));
$conversationid = empty($user2id) ? null : \core_message\api::get_conversation_between_users([$USER->id, $user2id]);
if (empty($conversationid) && !empty($user2id)) {
$PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'createConversationWithUser', [$user2id]);
} else if (!empty($conversationid)) {
$PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'showConversation', [$conversationid]);
} else {
$PAGE->requires->js_call_amd('core_message/message_drawer_helper', 'show');
}
echo $OUTPUT->footer();
exit();
}
// The only time we should get here is if it's an admin type user viewing another user's messages.
// Get the renderer and the information we are going to be use.
$renderer = $PAGE->get_renderer('core_message');
$requestedconversation = false;
+150
View File
@@ -736,6 +736,11 @@ function core_message_user_preferences() {
return $value;
}
);
$preferences['message_entertosend'] = array(
'type' => PARAM_BOOL,
'null' => NULL_NOT_ALLOWED,
'default' => false
);
$preferences['/^message_provider_([\w\d_]*)_logged(in|off)$/'] = array('isregex' => true, 'type' => PARAM_NOTAGS,
'null' => NULL_NOT_ALLOWED, 'default' => 'none',
'permissioncallback' => function ($user, $preferencename) {
@@ -763,3 +768,148 @@ function core_message_user_preferences() {
});
return $preferences;
}
/**
* Renders the popup.
*
* @param renderer_base $renderer
* @return string The HTML
*/
function core_message_render_navbar_output(\renderer_base $renderer) {
global $USER, $CFG;
// Early bail out conditions.
if (!isloggedin() || isguestuser() || user_not_fully_set_up($USER) ||
get_user_preferences('auth_forcepasswordchange') ||
(!$USER->policyagreed && !is_siteadmin() &&
($manager = new \core_privacy\local\sitepolicy\manager()) && $manager->is_defined())) {
return '';
}
$output = '';
// Add the messages popover.
if (!empty($CFG->messaging)) {
$unreadcount = \core_message\api::count_unread_conversations($USER);
$requestcount = \core_message\api::count_received_contact_requests($USER);
$context = [
'userid' => $USER->id,
'unreadcount' => $unreadcount + $requestcount
];
$output .= $renderer->render_from_template('core_message/message_popover', $context);
}
return $output;
}
/**
* Render the message drawer to be included in the top of the body of
* each page.
*
* @return string HTML
*/
function core_message_standard_after_main_region_html() {
global $USER, $CFG, $PAGE;
// Early bail out conditions.
if (empty($CFG->messaging) || !isloggedin() || isguestuser() || user_not_fully_set_up($USER) ||
get_user_preferences('auth_forcepasswordchange') ||
(!$USER->policyagreed && !is_siteadmin() &&
($manager = new \core_privacy\local\sitepolicy\manager()) && $manager->is_defined())) {
return '';
}
$renderer = $PAGE->get_renderer('core');
$individualconversationcount = \core_message\api::count_conversations(
$USER,
\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
true
);
$groupconversationcount = \core_message\api::count_conversations(
$USER,
\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
true
);
$systemcontext = \context_system::instance();
$usercontext = \context_user::instance($USER->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
$favouriteconversationcount = $ufservice->count_favourites_by_type('core_message', 'message_conversations', $systemcontext);
$requestcount = \core_message\api::count_received_contact_requests($USER);
$contactscount = \core_message\api::count_contacts($USER->id);
// Get the privacy settings options for being messaged.
$privacysetting = \core_message\api::get_user_privacy_messaging_preference($USER->id);
$choices = [];
$choices[] = [
'value' => \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS,
'text' => get_string('contactableprivacy_onlycontacts', 'message'),
'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS)
];
$choices[] = [
'value' => \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER,
'text' => get_string('contactableprivacy_coursemember', 'message'),
'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER)
];
if (!empty($CFG->messagingallusers)) {
// Add the MESSAGE_PRIVACY_SITE option when site-wide messaging between users is enabled.
$choices[] = [
'value' => \core_message\api::MESSAGE_PRIVACY_SITE,
'text' => get_string('contactableprivacy_site', 'message'),
'checked' => ($privacysetting == \core_message\api::MESSAGE_PRIVACY_SITE)
];
}
// Email settings.
$emailloggedin = get_user_preferences('message_provider_moodle_instantmessage_loggedin', 'none', $USER->id);
$emailloggedoff = get_user_preferences('message_provider_moodle_instantmessage_loggedoff', 'none', $USER->id);
$emailenabled = $emailloggedin == 'email' && $emailloggedoff == 'email';
// Enter to send.
$entertosend = get_user_preferences('message_entertosend', false, $USER->id);
return $renderer->render_from_template('core_message/message_drawer', [
'contactrequestcount' => $requestcount,
'loggedinuser' => [
'id' => $USER->id,
'midnight' => usergetmidnight(time())
],
'overview' => [
'messages' => [
'expanded' => empty($favouriteconversationcount) && empty($groupconversationcount),
'count' => [
'unread' => 0, // TODO: fix me.
'total' => $individualconversationcount
],
'placeholders' => array_fill(0, $individualconversationcount, true)
],
'groupmessages' => [
'expanded' => empty($favouriteconversationcount) && !empty($groupconversationcount),
'count' => [
'unread' => 0, // TODO: fix me.
'total' => $groupconversationcount
],
'placeholders' => array_fill(0, $groupconversationcount, true)
],
'favourites' => [
'expanded' => !empty($favouriteconversationcount),
'count' => [
'unread' => 0, // TODO: fix me.
'total' => $favouriteconversationcount
],
'placeholders' => array_fill(0, $favouriteconversationcount, true)
],
],
'contacts' => [
'sectioncontacts' => [
'placeholders' => array_fill(0, $contactscount > 50 ? 50 : $contactscount, true)
],
'sectionrequests' => [
'placeholders' => array_fill(0, $requestcount > 50 ? 50 : $requestcount, true)
],
],
'settings' => [
'privacy' => $choices,
'emailenabled' => $emailenabled,
'entertosend' => $entertosend
]
]);
}
@@ -1 +0,0 @@
define(["jquery","core/ajax","core/templates","core/str","core/notification","core/custom_interaction_events","core/popover_region_controller","core_message/message_repository","core/url"],function(a,b,c,d,e,f,g,h,i){var j={MARK_ALL_READ_BUTTON:'[data-action="mark-all-read"]',CONTENT:'[data-region="messages"]',CONTENT_ITEM_CONTAINER:'[data-region="message-content-item-container"]',EMPTY_MESSAGE:'[data-region="empty-message"]',COUNT_CONTAINER:'[data-region="count-container"]'},k=function(a){g.call(this,a),this.markAllReadButton=this.root.find(j.MARK_ALL_READ_BUTTON),this.content=this.root.find(j.CONTENT),this.userId=this.root.attr("data-userid"),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(j.COUNT_CONTAINER).html()};return k.prototype=Object.create(g.prototype),k.prototype.constructor=k,k.prototype.getContent=function(){return this.content},k.prototype.incrementOffset=function(){this.offset+=this.limit},k.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?d.get_string("hidemessagewindow","message").done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this)):this.unreadCount?d.get_string("showmessagewindowwithcount","message",this.unreadCount).done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this)):d.get_string("showmessagewindownonew","message").done(function(a){this.menuToggle.attr("aria-label",a)}.bind(this))},k.prototype.renderUnreadCount=function(){var a=this.root.find(j.COUNT_CONTAINER);this.unreadCount?(a.text(this.unreadCount),a.removeClass("hidden")):a.addClass("hidden")},k.prototype.hideUnreadCount=function(){this.root.find(j.COUNT_CONTAINER).addClass("hidden")},k.prototype.renderMessages=function(b,d){var e=[];return a.each(b,function(a,b){b.contexturl=i.relativeUrl("/message/index.php",{user:this.userId,id:b.userid}),b.profileurl=i.relativeUrl("/user/profile.php",{id:b.userid});var d=c.render("message_popup/message_content_item",b).then(function(a,b){return{html:a,js:b}});e.push(d)}.bind(this)),a.when.apply(a,e).then(function(){a.each(arguments,function(a,b){d.append(b.html),c.runTemplateJS(b.js)})})},k.prototype.loadMoreMessages=function(){if(this.isLoading||this.loadedAll)return a.Deferred().resolve();this.startLoading();var b={userid:this.userId,limit:this.limit,offset:this.offset},c=this.getContent();return h.query(b).then(function(a){var b=a.contacts;return this.loadedAll=!b.length||b.length<this.limit,this.initialLoad=!0,this.updateButtonAriaLabel(),!!b.length&&(this.incrementOffset(),this.renderMessages(b,c))}.bind(this)).always(function(){this.stopLoading()}.bind(this))},k.prototype.markAllAsRead=function(){return this.markAllReadButton.hasClass("loading")?a.Deferred().resolve():(this.markAllReadButton.addClass("loading"),h.markAllAsRead({useridto:this.userId}).then(function(){this.unreadCount=0,this.hideUnreadCount(),this.getContent().find(j.CONTENT_ITEM_CONTAINER).removeClass("unread")}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this)))},k.prototype.registerEventListeners=function(){f.define(this.root,[f.events.keyboardActivate]),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.initialLoad||this.loadMoreMessages()}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel()}.bind(this)),this.root.on(f.events.scrollBottom,function(){this.loadMoreMessages()}.bind(this)),this.root.on(f.events.activate,j.MARK_ALL_READ_BUTTON,function(a,b){this.markAllAsRead(),a.stopPropagation(),b.originalEvent.preventDefault()}.bind(this)),f.define(this.getContentContainer(),[f.events.scrollLock]),a(document).on("messagearea:conversationselected",function(){this.unreadCount--,this.renderUnreadCount()}.bind(this))},k});
@@ -1,300 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Controls the message popover in the nav bar.
*
* See template: message_popup/message_popover
*
* @module message_popup/message_popover_controller
* @class message_popover_controller
* @package message_popup
* @copyright 2016 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/templates', 'core/str',
'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',
'core_message/message_repository', 'core/url'],
function($, Ajax, Templates, Str, Notification, CustomEvents,
PopoverController, MessageRepo, URL) {
var SELECTORS = {
MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]',
CONTENT: '[data-region="messages"]',
CONTENT_ITEM_CONTAINER: '[data-region="message-content-item-container"]',
EMPTY_MESSAGE: '[data-region="empty-message"]',
COUNT_CONTAINER: '[data-region="count-container"]',
};
/**
* Constructor for the MessagePopoverController.
* Extends PopoverRegionController.
*
* @param {object} element jQuery object root element of the popover
*/
var MessagePopoverController = function(element) {
// Initialise base class.
PopoverController.call(this, element);
this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);
this.content = this.root.find(SELECTORS.CONTENT);
this.userId = this.root.attr('data-userid');
this.limit = 20;
this.offset = 0;
this.loadedAll = false;
this.initialLoad = false;
// Let's find out how many unread messages there are.
this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();
};
/**
* Clone the parent prototype.
*/
MessagePopoverController.prototype = Object.create(PopoverController.prototype);
/**
* Make sure the constructor is set correctly.
*/
MessagePopoverController.prototype.constructor = MessagePopoverController;
/**
* Get the element holding the messages.
*
* @method getContent
* @return {object} jQuery element
*/
MessagePopoverController.prototype.getContent = function() {
return this.content;
};
/**
* Increment the offset.
*
* @method incrementOffset
*/
MessagePopoverController.prototype.incrementOffset = function() {
this.offset += this.limit;
};
/**
* Set the correct aria label on the menu toggle button to be read out by screen
* readers. The message will indicate the state of the unread messages.
*
* @method updateButtonAriaLabel
*/
MessagePopoverController.prototype.updateButtonAriaLabel = function() {
if (this.isMenuOpen()) {
Str.get_string('hidemessagewindow', 'message').done(function(string) {
this.menuToggle.attr('aria-label', string);
}.bind(this));
} else {
if (this.unreadCount) {
Str.get_string('showmessagewindowwithcount', 'message', this.unreadCount).done(function(string) {
this.menuToggle.attr('aria-label', string);
}.bind(this));
} else {
Str.get_string('showmessagewindownonew', 'message').done(function(string) {
this.menuToggle.attr('aria-label', string);
}.bind(this));
}
}
};
/**
* Show the unread message count badge on the menu toggle if there
* are unread messages, otherwise hide it.
*
* @method renderUnreadCount
*/
MessagePopoverController.prototype.renderUnreadCount = function() {
var element = this.root.find(SELECTORS.COUNT_CONTAINER);
if (this.unreadCount) {
element.text(this.unreadCount);
element.removeClass('hidden');
} else {
element.addClass('hidden');
}
};
/**
* Hide the unread message count badge on the menu toggle.
*
* @method hideUnreadCount
*/
MessagePopoverController.prototype.hideUnreadCount = function() {
this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');
};
/**
* Render the message data with the appropriate template and add it to the DOM.
*
* @method renderMessages
* @param {array} messages Message data
* @param {object} container jQuery object the container to append the rendered messages
* @return {object} jQuery promise that is resolved when all messages have been
* rendered and added to the DOM
*/
MessagePopoverController.prototype.renderMessages = function(messages, container) {
var promises = [];
$.each(messages, function(index, message) {
message.contexturl = URL.relativeUrl('/message/index.php', {
user: this.userId,
id: message.userid,
});
message.profileurl = URL.relativeUrl('/user/profile.php', {
id: message.userid,
});
var promise = Templates.render('message_popup/message_content_item', message)
.then(function(html, js) {
return {html: html, js: js};
});
promises.push(promise);
}.bind(this));
return $.when.apply($, promises).then(function() {
// Each of the promises in the when will pass its results as an argument to the function.
// The order of the arguments will be the order that the promises are passed to when()
// i.e. the first promise's results will be in the first argument.
$.each(arguments, function(index, argument) {
container.append(argument.html);
Templates.runTemplateJS(argument.js);
});
return;
});
};
/**
* Send a request for more messages from the server, if we aren't already
* loading some and haven't already loaded all of them.
*
* @method loadMoreMessages
* @return {object} jQuery promise that is resolved when messages have been
* retrieved and added to the DOM
*/
MessagePopoverController.prototype.loadMoreMessages = function() {
if (this.isLoading || this.loadedAll) {
return $.Deferred().resolve();
}
this.startLoading();
var request = {
userid: this.userId,
limit: this.limit,
offset: this.offset,
};
var container = this.getContent();
return MessageRepo.query(request).then(function(result) {
var messages = result.contacts;
this.loadedAll = !messages.length || messages.length < this.limit;
this.initialLoad = true;
this.updateButtonAriaLabel();
if (messages.length) {
this.incrementOffset();
return this.renderMessages(messages, container);
}
return false;
}.bind(this))
.always(function() {
this.stopLoading();
}.bind(this));
};
/**
* Send a request to the server to mark all unread messages as read and update
* the unread count and unread messages elements appropriately.
*
* @method markAllAsRead
* @return {Promise}
*/
MessagePopoverController.prototype.markAllAsRead = function() {
if (this.markAllReadButton.hasClass('loading')) {
return $.Deferred().resolve();
}
this.markAllReadButton.addClass('loading');
return MessageRepo.markAllAsRead({useridto: this.userId})
.then(function() {
this.unreadCount = 0;
this.hideUnreadCount();
this.getContent().find(SELECTORS.CONTENT_ITEM_CONTAINER).removeClass('unread');
}.bind(this))
.always(function() {
this.markAllReadButton.removeClass('loading');
}.bind(this));
};
/**
* Add all of the required event listeners for this messages popover.
*
* @method registerEventListeners
*/
MessagePopoverController.prototype.registerEventListeners = function() {
CustomEvents.define(this.root, [
CustomEvents.events.keyboardActivate,
]);
// Update the message information when the menu is opened.
this.root.on(this.events().menuOpened, function() {
this.hideUnreadCount();
this.updateButtonAriaLabel();
if (!this.initialLoad) {
this.loadMoreMessages();
}
}.bind(this));
// Update the message information when the menu is opened.
this.root.on(this.events().menuClosed, function() {
this.renderUnreadCount();
this.updateButtonAriaLabel();
}.bind(this));
// Load more messages when we scroll to the bottom of the open menu.
this.root.on(CustomEvents.events.scrollBottom, function() {
this.loadMoreMessages();
}.bind(this));
// Mark all messages as read when button is activated.
this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {
this.markAllAsRead();
e.stopPropagation();
data.originalEvent.preventDefault();
}.bind(this));
// Stop mouse scroll from propagating to the window element and
// scrolling the page.
CustomEvents.define(this.getContentContainer(), [
CustomEvents.events.scrollLock
]);
// Check if we have marked a conversation as read in the messaging area.
$(document).on('messagearea:conversationselected', function() {
this.unreadCount--;
this.renderUnreadCount();
}.bind(this));
};
return MessagePopoverController;
});
-15
View File
@@ -43,21 +43,6 @@ function message_popup_render_navbar_output(\renderer_base $renderer) {
$output = '';
// Add the messages popover.
if (!empty($CFG->messaging)) {
$unreadcount = \core_message\api::count_unread_conversations($USER);
$context = [
'userid' => $USER->id,
'unreadcount' => $unreadcount,
'urls' => [
'seeall' => (new moodle_url('/message/index.php'))->out(),
'writeamessage' => (new moodle_url('/message/index.php', ['contactsfirst' => 1]))->out(),
'preferences' => (new moodle_url('/message/edit.php', ['id' => $USER->id]))->out(),
],
];
$output .= $renderer->render_from_template('message_popup/message_popover', $context);
}
// Add the notifications popover.
$enabled = \core_message\api::is_processor_enabled("popup");
if ($enabled) {
@@ -1,85 +0,0 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template message_output_popup/message_popover
This template will render the message popover for the navigation bar.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{
"userid": 3,
"urls": {
"preferences": "http://www.moodle.com"
}
}
}}
{{< core/popover_region }}
{{$classes}}popover-region-messages{{/classes}}
{{$attributes}}id="nav-message-popover-container" data-userid="{{userid}}"{{/attributes}}
{{$togglelabel}}{{#str}} showmessagewindownonew, message {{/str}}{{/togglelabel}}
{{$togglecontent}}
{{#pix}} t/message, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}}
<div class="count-container {{^unreadcount}}hidden{{/unreadcount}}" data-region="count-container">{{unreadcount}}</div>
{{/togglecontent}}
{{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}}
{{$headertext}}{{#str}} messages, message {{/str}}{{/headertext}}
{{$headeractions}}
<div class="newmessage-link">
<a href="{{{urls.writeamessage}}}">{{#str}} newmessage, message {{/str}}
</a>
</div>
<a class="mark-all-read-button"
href="#"
role="button"
title="{{#str}} markallread {{/str}}"
data-action="mark-all-read">
<span class="normal-icon">{{#pix}} t/markasread, core, {{#str}} markallread {{/str}} {{/pix}}</span>
{{> core/loading }}
</a>
<a href="{{{urls.preferences}}}"
title="{{#str}} messagepreferences, message {{/str}}">
{{#pix}} i/settings, core, {{#str}} messagepreferences, message {{/str}} {{/pix}}
</a>
{{/headeractions}}
{{$content}}
<div class="messages" data-region="messages" role="log" aria-busy="false" aria-atomic="false" aria-relevant="additions"></div>
<div class="empty-message" data-region="empty-message" tabindex="0">{{#str}} nomessages, message {{/str}}</div>
{{/content}}
{{/ core/popover_region }}
{{#js}}
require(['jquery', 'message_popup/message_popover_controller'], function($, controller) {
var container = $('#nav-message-popover-container');
var controller = new controller(container);
controller.registerEventListeners();
controller.registerListNavigationEventListeners();
});
{{/js}}
@@ -1,17 +0,0 @@
@core_message @message_popup
Feature: Message popover preferences
In order to modify my message preferences
As a user
I can navigate to the preferences page from the popover
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | 1 | user1@asd.com |
@javascript
Scenario: User navigates to preferences page
Given I log in as "user1"
And I open the message popover
When I follow "Message preferences"
Then I should see "Message preferences"
@@ -1,60 +0,0 @@
@core_message @message_popup @javascript
Feature: Message popover unread messages
In order to be kept informed
As a user
I am notified about unread messages from other users
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
And the following config values are set as admin:
| messaging | 1 |
| messagingallusers | 1 |
And I log in as "student2"
And I send "Test message" message to "Student 1" user
And I log out
Scenario: Message popover shows correct unread count
When I log in as "student2"
And I send "Test message 2" message to "Student 1" user
And I log out
And I log in as "student1"
# Confirm the popover is saying 1 unread conversation.
Then I should see "1" in the "#nav-message-popover-container [data-region='count-container']" "css_element"
# Open the popover.
And I open the message popover
# Confirm the conversation is visible.
And I should see "Test message 2" in the "#nav-message-popover-container" "css_element"
# Confirm the count of unread messages in the conversation is correct.
And I should see "2" in the "#nav-message-popover-container [data-region='unread-count']" "css_element"
Scenario: Clicking a message marks it as read
When I log in as "student1"
# Open the popover.
And I open the message popover
# Click on the conversation.
And I click on "[aria-label='View unread messages with Student 2']" "css_element" in the "#nav-message-popover-container" "css_element"
# Confirm the count element is hidden (i.e. there are no unread messages).
Then "[data-region='count-container']" "css_element" in the "#nav-message-popover-container" "css_element" should not be visible
# Confirm the message was loaded in the messaging page.
And I should see "Test message" in the "[data-region='message-text']" "css_element"
Scenario: Mark all messages as read
When I log in as "student1"
# Open the popover.
And I open the message popover
# Click the mark all as read button.
And I click on "[data-action='mark-all-read']" "css_element" in the "#nav-message-popover-container" "css_element"
# Refresh the page to make sure we send a new request for the unread count.
And I reload the page
# Confirm the count element is hidden (i.e. there are no unread messages).
Then "[data-region='count-container']" "css_element" in the "#nav-message-popover-container" "css_element" should not be visible
+70
View File
@@ -0,0 +1,70 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer
This template will render the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div
id="message-drawer-{{uniqid}}"
class="message-drawer bg-light hidden"
aria-expanded="false"
aria-hidden="true"
data-region="message-drawer"
role="region"
>
<div class="header-container position-relative" data-region="header-container">
{{> core_message/message_drawer_view_contacts_header }}
{{> core_message/message_drawer_view_conversation_header }}
{{> core_message/message_drawer_view_overview_header }}
{{> core_message/message_drawer_view_search_header }}
{{> core_message/message_drawer_view_settings_header }}
</div>
<div class="body-container position-relative" data-region="body-container">
{{> core_message/message_drawer_view_contact_body }}
{{> core_message/message_drawer_view_contacts_body }}
{{> core_message/message_drawer_view_conversation_body }}
{{> core_message/message_drawer_view_group_info_body }}
{{> core_message/message_drawer_view_overview_body }}
{{> core_message/message_drawer_view_search_body }}
{{> core_message/message_drawer_view_settings_body }}
</div>
<div class="footer-container position-relative" data-region="footer-container">
{{> core_message/message_drawer_view_conversation_footer }}
</div>
</div>
{{#js}}
require(['jquery', 'core_message/message_drawer'], function($, MessageDrawer) {
var root = $('#message-drawer-{{uniqid}}');
MessageDrawer.init(root);
});
{{/js}}
@@ -0,0 +1,71 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_contacts_list
This template will render a list of contacts for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{#contacts}}
<a
href="#"
class="p-2 d-flex list-group-item list-group-item-action border-0"
data-route="view-conversation"
{{#conversationid}}
data-route-param-1="{{.}}"
{{/conversationid}}
{{^conversationid}}
data-route-param-1="false"
data-route-param-2="create"
data-route-param-3="{{id}}"
{{/conversationid}}
data-contact-user-id="{{id}}"
data-region="contact"
>
<img
class="rounded-circle"
src="{{{profileimageurl}}}"
alt="{{fullname}}"
aria-hidden="true"
style="height: 38px"
>
{{#showonlinestatus}}
<span class="contact-status {{#isonline}}online{{/isonline}}"></span>
{{/showonlinestatus}}
<h6 class="ml-2" data-region="searchable">{{fullname}}</h6>
<div
class="ml-auto align-self-end {{^isblocked}}hidden{{/isblocked}}"
data-region="block-icon-container"
>
{{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
</div>
</a>
{{/contacts}}
@@ -0,0 +1,48 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_contacts_list_item_placeholder
This template will render a placeholder loading item for a contact in
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div class="p-2 d-flex list-group-item border-0" data-region="placeholder">
<div
class="rounded-circle bg-pulse-grey"
style="height: 38px; width: 38px"
>
</div>
<div
class="ml-2 bg-pulse-grey w-50"
style="height: 20px;"
>
</div>
</div>
@@ -0,0 +1,93 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_conversations_list
This template will render a list of conversations for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{#conversations}}
<a
href="#"
class="p-2 d-flex list-group-item list-group-item-action border-0 align-items-center"
data-conversation-id="{{id}}"
{{#userid}}
data-user-id="{{.}}"
{{/userid}}
>
{{#imageurl}}
<img
class="rounded-circle"
src="{{{.}}}"
alt="{{name}}"
aria-hidden="true"
style="height: 38px"
>
{{/imageurl}}
{{#showonlinestatus}}
<span class="contact-status {{#isonline}}online{{/isonline}}"></span>
{{/showonlinestatus}}
<div class="w-100 text-truncate ml-2">
<div class="d-flex">
<strong class="m-0 text-truncate">{{name}}</strong>
<span class="{{^isblocked}}hidden{{/isblocked}}" data-region="contact-icon-blocked">
{{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
</span>
<div
class="ml-auto small {{^lastmessagedate}}hidden{{/lastmessagedate}}"
data-region="last-message-date"
>
{{#lastmessagedate}}
{{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
{{/lastmessagedate}}
</div>
</div>
{{#subname}}
<p class="m-0 text-truncate">{{.}}</p>
{{/subname}}
<p class="m-0 font-weight-light text-truncate" data-region="last-message">
{{#sentfromcurrentuser}}{{#str}} you, core_message {{/str}}{{/sentfromcurrentuser}}
{{lastmessage}}
</p>
</div>
<span
class="badge badge-pill badge-primary bg-primary rounded-circle {{^unreadcount}}hidden{{/unreadcount}}"
data-region="unread-count"
>
{{unreadcount}}
</span>
<div class="text-muted">
{{> core_message/message_drawer_icon_forward }}
</div>
</a>
{{/conversations}}
@@ -0,0 +1,65 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_conversations_list_item_placeholder
This template will render a placeholder loading element for a message in
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div class="p-2 d-flex list-group-item border-0" data-region="placeholder">
<div
class="rounded-circle bg-pulse-grey"
style="height: 38px; width: 38px"
>
</div>
<div class="ml-2 align-self-center" style="flex: 1">
<div
class="bg-pulse-grey w-50"
style="height: 15px;"
>
</div>
<div
class="bg-pulse-grey mt-1"
style="height: 15px; width: 90%"
>
</div>
</div>
<div
class="bg-pulse-grey align-self-center"
style="height: 15px; width: 15px"
>
</div>
<div
class="ml-2 bg-pulse-grey align-self-center"
style="height: 15px; width: 15px"
>
</div>
</div>
@@ -0,0 +1,37 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_icon_back
This template will render the back icon for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<span class="dir-rtl-hide">{{#pix}} i/previous, core {{/pix}}</span>
<span class="dir-ltr-hide">{{#pix}} i/next, core {{/pix}}</span>
@@ -0,0 +1,37 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_icon_forward
This template will render the forward icon for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<span class="dir-rtl-hide">{{#pix}} i/next, core {{/pix}}</span>
<span class="dir-ltr-hide">{{#pix}} i/previous, core {{/pix}}</span>
@@ -0,0 +1,58 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_lazy_load_list
This template will render a lazy loaded list for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div
class="{{$rootclasses}}{{/rootclasses}}"
style="overflow-y: auto"
aria-live="polite"
data-region="lazy-load-list"
data-user-id="{{loggedinuser.id}}"
{{$rootattributes}}{{/rootattributes}}
>
<div class="hidden text-center p-2" data-region="empty-message-container">
<p class="text-muted mt-2">
{{$emptymessage}}{{/emptymessage}}
</p>
</div>
<div class="hidden list-group" data-region="content-container">
{{$content}}{{/content}}
</div>
<div class="list-group" data-region="placeholder-container">
{{$placeholder}}{{/placeholder}}
</div>
<div class="w-100 text-center p-3 hidden" data-region="loading-icon-container" >
{{> core/loading }}
</div>
</div>
@@ -0,0 +1,97 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_messages_list
This template will render a list of messages for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{#messages}}
<a
href="#"
class="p-2 d-flex list-group-item list-group-item-action border-0 align-items-center"
data-route="view-conversation"
{{#conversationid}}
data-route-param-1="{{.}}"
{{/conversationid}}
{{^conversationid}}
data-route-param-1="false"
data-route-param-2="create"
data-route-param-3="{{userid}}"
{{/conversationid}}
data-conversation-id="{{conversationid}}"
>
<img
class="rounded-circle"
src="{{{profileimageurl}}}"
alt="{{fullname}}"
aria-hidden="true"
style="height: 38px"
>
{{#showonlinestatus}}
<span class="contact-status {{#isonline}}online{{/isonline}}"></span>
{{/showonlinestatus}}
<div class="w-100 text-truncate ml-2">
<div class="d-flex">
<strong class="m-0">
{{fullname}}
<span class="{{^isblocked}}hidden{{/isblocked}}" data-region="contact-icon-blocked">
{{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
</span>
</strong>
<div
class="ml-auto small {{^lastmessagedate}}hidden{{/lastmessagedate}}"
data-region="last-message-date"
>
{{#lastmessagedate}}
{{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
{{/lastmessagedate}}
</div>
</div>
<p class="m-0 font-weight-light text-truncate" data-region="last-message">
{{#sentfromcurrentuser}}{{#str}} you, core_message {{/str}}{{/sentfromcurrentuser}}
{{lastmessage}}
</p>
</div>
<span
class="badge badge-pill badge-primary bg-primary rounded-circle {{^unreadcount}}hidden{{/unreadcount}}"
data-region="unread-count"
>
{{unreadcount}}
</span>
<div class="text-muted">
{{> core_message/message_drawer_icon_forward }}
</div>
</a>
{{/messages}}
@@ -0,0 +1,65 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_messages_list_item_placeholder
This template will render a placeholder loading element for a message in
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div class="p-2 d-flex list-group-item border-0" data-region="placeholder">
<div
class="rounded-circle bg-pulse-grey"
style="height: 38px; width: 38px"
>
</div>
<div class="ml-2 align-self-center" style="flex: 1">
<div
class="bg-pulse-grey w-50"
style="height: 15px;"
>
</div>
<div
class="bg-pulse-grey mt-1"
style="height: 15px; width: 90%"
>
</div>
</div>
<div
class="bg-pulse-grey align-self-center"
style="height: 15px; width: 15px"
>
</div>
<div
class="ml-2 bg-pulse-grey align-self-center"
style="height: 15px; width: 15px"
>
</div>
</div>
@@ -0,0 +1,65 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_non_contacts_list
This template will render a list of non contacts for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{#noncontacts}}
<a
href="#"
class="p-2 d-flex list-group-item list-group-item-action border-0"
data-route="view-conversation"
{{#conversationid}}
data-route-param-1="{{.}}"
{{/conversationid}}
{{^conversationid}}
data-route-param-1="false"
data-route-param-2="create"
data-route-param-3="{{id}}"
{{/conversationid}}
>
<img
class="rounded-circle"
src="{{{profileimageurl}}}"
alt="{{fullname}}"
aria-hidden="true"
style="height: 38px"
>
<h6 class="ml-2" data-region="searchable">{{fullname}}</h6>
{{#isblocked}}
<div class="ml-auto align-self-end">
{{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
</div>
{{/isblocked}}
</a>
{{/noncontacts}}
@@ -0,0 +1,44 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contact_body
This template will render the body section of the contact page in the
message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div
class="hidden"
data-region="view-contact"
aria-hidden="true"
>
<div class="p-2 pt-3" data-region="content-container"></div>
</div>
@@ -0,0 +1,95 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contact_body_content
This template will render the content for the body section of the contact
page in the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<a class="align-self-center" href="#" data-route-back>
{{> core_message/message_drawer_icon_back }}
</a>
<div class="px-3 pb-3">
<div class="text-center">
<img
class="rounded-circle"
src="{{profileimageurl}}"
aria-hidden="true"
alt="{{#str}} pictureof, moodle, {{fullname}} {{/str}}"
title="{{#str}} pictureof, moodle, {{fullname}} {{/str}}"
style="height: 100px; width: 100px"
>
</div>
<h2 class="mt-2 text-center text-truncate">{{fullname}}</h2>
<div class="px-4 mt-4">
<button
type="button"
class="btn btn-primary btn-block {{^isblocked}}hidden{{/isblocked}}"
data-route="view-conversation"
data-route-param-1=""
data-route-param-2="unblock"
data-route-param-3="{{id}}"
>
{{#str}}unblockuser, message{{/str}}
</button>
<button
type="button"
class="btn btn-danger btn-block {{#isblocked}}hidden{{/isblocked}}"
data-route="view-conversation"
data-route-param-1=""
data-route-param-2="block"
data-route-param-3="{{id}}"
>
{{#str}}blockuser, message{{/str}}
</button>
<button
type="button"
class="btn btn-danger btn-block {{^iscontact}}hidden{{/iscontact}}"
data-route="view-conversation"
data-route-param-1=""
data-route-param-2="remove-contact"
data-route-param-3="{{id}}"
>
{{#str}}removefromyourcontacts, message{{/str}}
</button>
<button
type="button"
class="btn btn-primary btn-block {{#iscontact}}hidden{{/iscontact}}"
data-route="view-conversation"
data-route-param-1=""
data-route-param-2="add-contact"
data-route-param-3="{{id}}"
>
{{#str}}addtoyourcontacts, message{{/str}}
</button>
</div>
</div>
@@ -0,0 +1,96 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contacts_body
This template will render the body content of the contacts page in the
message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div class="hidden h-100" data-region="view-contacts" aria-hidden="true" data-user-id="{{loggedinuser.id}}">
<div class="d-flex flex-column h-100">
<div class="p-3 border-bottom">
<ul class="nav nav-pills nav-fill" role="tablist">
<li class="nav-item">
<a
id="contacts-tab-{{uniqid}}"
class="nav-link active"
href="#contacts-tab-panel-{{uniqid}}"
data-toggle="tab"
data-action="show-contacts-section"
role="tab"
aria-controls="contacts-tab-panel-{{uniqid}}"
aria-selected="true"
>
{{#str}} contacts, core_message {{/str}}
</a>
</li>
<li class="nav-item">
<a
id="requests-tab-{{uniqid}}"
class="nav-link"
href="#requests-tab-panel-{{uniqid}}"
data-toggle="tab"
data-action="show-requests-section"
role="tab"
aria-controls="requests-tab-panel-{{uniqid}}"
aria-selected="false"
>
{{#str}} requests {{/str}}
<span class="badge bg-primary ml-2 {{^contactrequestcount}}hidden{{/contactrequestcount}}" data-region="contact-request-count">
{{contactrequestcount}}
</span>
</a>
</li>
</ul>
</div>
<div class="tab-content d-flex flex-column">
{{< core_message/message_drawer_view_contacts_body_section_contacts }}
{{$rootclasses}}tab-pane fade show active h-100{{/rootclasses}}
{{$rootattributes}}
id="contacts-tab-panel-{{uniqid}}"
data-section="contacts"
role="tabpanel"
aria-labelledby="contacts-tab-{{uniqid}}"
{{/rootattributes}}
{{/ core_message/message_drawer_view_contacts_body_section_contacts }}
{{< core_message/message_drawer_view_contacts_body_section_requests }}
{{$rootclasses}}tab-pane fade h-100{{/rootclasses}}
{{$rootattributes}}
id="requests-tab-panel-{{uniqid}}"
data-section="requests"
role="tabpanel"
aria-labelledby="requests-tab-{{uniqid}}"
{{/rootattributes}}
{{/ core_message/message_drawer_view_contacts_body_section_requests }}
</div>
</div>
</div>
@@ -0,0 +1,48 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contacts_body_section_contacts
This template will render the contacts section of the body of the contacts
page in the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{< core_message/message_drawer_lazy_load_list }}
{{$emptymessage}}{{#str}} nocontactsgetstarted, core_message {{/str}}{{/emptymessage}}
{{$placeholder}}
{{#contacts}}
{{#sectionrequests}}
{{#placeholders}}
{{> core_message/message_drawer_contacts_list_item_placeholder }}
{{/placeholders}}
{{/sectionrequests}}
{{/contacts}}
{{/placeholder}}
{{/ core_message/message_drawer_lazy_load_list }}
@@ -0,0 +1,48 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contacts_body_section_requests
This template will render the the requests section of the body of the contacts
page in the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{< core_message/message_drawer_lazy_load_list }}
{{$emptymessage}}{{#str}} nocontactrequests, core_message {{/str}}{{/emptymessage}}
{{$placeholder}}
{{#contacts}}
{{#sectioncontacts}}
{{#placeholders}}
{{> core_message/message_drawer_contacts_list_item_placeholder }}
{{/placeholders}}
{{/sectioncontacts}}
{{/contacts}}
{{/placeholder}}
{{/ core_message/message_drawer_lazy_load_list }}
@@ -0,0 +1,71 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_contacts_list
This template will render a list of contacts for the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
{{#requests}}
<a
href="#"
class="p-2 d-flex list-group-item list-group-item-action border-0 align-items-center"
data-route="view-conversation"
{{#conversationid}}
data-route-param-1="{{.}}"
{{/conversationid}}
{{^conversationid}}
data-route-param-1="false"
data-route-param-2="create"
data-route-param-3="{{id}}"
{{/conversationid}}
data-request-id="{{id}}"
data-region="contact-request"
>
<img
class="rounded-circle"
src="{{{profileimageurl}}}"
alt="{{fullname}}"
aria-hidden="true"
style="height: 38px"
>
<div class="w-100 text-truncate ml-2">
<div class="d-flex">
<strong class="m-0">
{{fullname}}
</strong>
</div>
<p class="m-0 font-weight-light text-truncate">
{{#str}} wouldliketocontactyou, core_message {{/str}}
</p>
</div>
</a>
{{/requests}}
@@ -0,0 +1,53 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contacts_header
This template will render the header for the contacts page of
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div class="hidden border-bottom px-2 py-3" aria-hidden="true" data-region="view-contacts">
<div class="d-flex align-items-center">
<div class="align-self-stretch">
<a class="h-100 d-flex align-items-center mr-2" href="#" data-route-back>
{{> core_message/message_drawer_icon_back }}
</a>
</div>
<div>
{{#str}} contacts, core_message {{/str}}
</div>
<div class="ml-auto">
<a href="#" data-route="view-search">
{{#pix}} a/search, core {{/pix}}
</a>
</div>
</div>
</div>
@@ -0,0 +1,56 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_view_contacts_section
This template will render a generic "section" on the contacts page of
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
* userid The logged in user id
* urls The URLs for the popover
Example context (json):
{}
}}
<div
data-region="{{$region}}{{/region}}"
data-user-id="{{loggedinuser.id}}"
>
<div
id="{{$region}}{{/region}}-target"
aria-labelledby="{{$region}}{{/region}}"
>
<div class="hidden text-center p-2" data-region="empty-message-container">
{{$emptymessage}}{{/emptymessage}}
</div>
<div class="hidden list-group" data-region="contacts-content-container">
{{$content}}{{/content}}
</div>
<div class="list-group" data-region="placeholder-container">
{{$placeholder}}{{/placeholder}}
</div>
</div>
</div>
@@ -0,0 +1,60 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_message/message_drawer_conversation_body
This template will render the body container for the conversation page in
the message drawer.
Classes required for JS:
* none
Data attributes required for JS:
* All data attributes are required
Context variables required for this template:
*
Example context (json):
{}
}}
<div
class="view-conversation hidden h-100"
aria-hidden="true"
data-region="view-conversation"
data-user-id="{{loggedinuser.id}}"
data-midnight="{{loggedinuser.midnight}}"
style="overflow-y: auto; overflow-x: hidden"
>
<div class="position-relative h-100" data-region="content-container" style="overflow-y: auto; overflow-x: hidden">
<div class="p-3 text-center hidden" data-region="contact-request-sent-message-container">
<p class="m-0">{{#str}} contactrequestsent, core_message {{/str}}</p>
<p class="font-italic font-weight-light" data-region="text"></p>
</div>
<div class="content-message-container hidden h-100 px-2 pb-2 pt-0" data-region="content-message-container" role="log" style="overflow-y: auto; overflow-x: hidden">
<div class="hidden text-center p-3" data-region="more-messages-loading-icon-container">{{> core/loading }}</div>
</div>
<div class="p-4 w-100 h-100 hidden position-absolute" data-region="confirm-dialogue-container" style="top: 0; background: rgba(0,0,0,0.3);">
{{> core_message/message_drawer_view_conversation_body_confirm_dialogue }}
</div>
<div class="px-2 pb-2 pt-0" data-region="content-placeholder">
{{> core_message/message_drawer_view_conversation_body_placeholder }}
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More