ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PKS\Clz''externallib.phpnu[. use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die(); /** * External message popup API * * @package message_popup * @category external * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot . "/message/lib.php"); /** * Message external functions * * @package message_popup * @category external * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class message_popup_external extends external_api { /** * Get popup notifications parameters description. * * @return external_function_parameters * @since 3.2 */ public static function get_popup_notifications_parameters() { return new external_function_parameters( array( 'useridto' => new external_value(PARAM_INT, 'the user id who received the message, 0 for current user'), 'newestfirst' => new external_value( PARAM_BOOL, 'true for ordering by newest first, false for oldest first', VALUE_DEFAULT, true), 'limit' => new external_value(PARAM_INT, 'the number of results to return', VALUE_DEFAULT, 0), 'offset' => new external_value(PARAM_INT, 'offset the result set by a given amount', VALUE_DEFAULT, 0) ) ); } /** * Get notifications function. * * @since 3.2 * @throws invalid_parameter_exception * @throws moodle_exception * @param int $useridto the user id who received the message * @param bool $newestfirst true for ordering by newest first, false for oldest first * @param int $limit the number of results to return * @param int $offset offset the result set by a given amount * @return \core_external\external_description */ public static function get_popup_notifications($useridto, $newestfirst, $limit, $offset) { global $USER, $PAGE; $params = self::validate_parameters( self::get_popup_notifications_parameters(), array( 'useridto' => $useridto, 'newestfirst' => $newestfirst, 'limit' => $limit, 'offset' => $offset, ) ); $context = context_system::instance(); self::validate_context($context); $useridto = $params['useridto']; $newestfirst = $params['newestfirst']; $limit = $params['limit']; $offset = $params['offset']; $issuperuser = has_capability('moodle/site:readallmessages', $context); $renderer = $PAGE->get_renderer('core_message'); if (empty($useridto)) { $useridto = $USER->id; } // Check if the current user is the sender/receiver or just a privileged user. if ($useridto != $USER->id and !$issuperuser) { throw new moodle_exception('accessdenied', 'admin'); } if (!empty($useridto)) { if (!core_user::is_real_user($useridto)) { throw new moodle_exception('invaliduser'); } } $sort = $newestfirst ? 'DESC' : 'ASC'; $notifications = \message_popup\api::get_popup_notifications($useridto, $sort, $limit, $offset); $notificationcontexts = []; if ($notifications) { foreach ($notifications as $notification) { $notificationoutput = new \message_popup\output\popup_notification($notification); $notificationcontext = $notificationoutput->export_for_template($renderer); // Keep this for BC. $notificationcontext->deleted = false; $notificationcontexts[] = $notificationcontext; } } return array( 'notifications' => $notificationcontexts, 'unreadcount' => \message_popup\api::count_unread_popup_notifications($useridto), ); } /** * Get notifications return description. * * @return external_single_structure * @since 3.2 */ public static function get_popup_notifications_returns() { return new external_single_structure( array( 'notifications' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'Notification id (this is not guaranteed to be unique within this result set)'), 'useridfrom' => new external_value(PARAM_INT, 'User from id'), 'useridto' => new external_value(PARAM_INT, 'User to id'), 'subject' => new external_value(PARAM_TEXT, 'The notification subject'), 'shortenedsubject' => new external_value(PARAM_TEXT, 'The notification subject shortened with ellipsis'), 'text' => new external_value(PARAM_RAW, 'The message text formated'), 'fullmessage' => new external_value(PARAM_RAW, 'The message'), 'fullmessageformat' => new external_format_value('fullmessage'), 'fullmessagehtml' => new external_value(PARAM_RAW, 'The message in html'), 'smallmessage' => new external_value(PARAM_RAW, 'The shorten message'), 'contexturl' => new external_value(PARAM_RAW, 'Context URL'), 'contexturlname' => new external_value(PARAM_TEXT, 'Context URL link name'), 'timecreated' => new external_value(PARAM_INT, 'Time created'), 'timecreatedpretty' => new external_value(PARAM_TEXT, 'Time created in a pretty format'), 'timeread' => new external_value(PARAM_INT, 'Time read'), 'read' => new external_value(PARAM_BOOL, 'notification read status'), 'deleted' => new external_value(PARAM_BOOL, 'notification deletion status'), 'iconurl' => new external_value(PARAM_URL, 'URL for notification icon'), 'component' => new external_value(PARAM_TEXT, 'The component that generated the notification', VALUE_OPTIONAL), 'eventtype' => new external_value(PARAM_TEXT, 'The type of notification', VALUE_OPTIONAL), 'customdata' => new external_value(PARAM_RAW, 'Custom data to be passed to the message processor. The data here is serialised using json_encode().', VALUE_OPTIONAL), ), 'message' ) ), 'unreadcount' => new external_value(PARAM_INT, 'the number of unread message for the given user'), ) ); } /** * Get unread notification count parameters description. * * @return external_function_parameters * @since 3.2 */ public static function get_unread_popup_notification_count_parameters() { return new external_function_parameters( array( 'useridto' => new external_value(PARAM_INT, 'the user id who received the message, 0 for any user', VALUE_REQUIRED), ) ); } /** * Get unread notification count function. * * @since 3.2 * @throws invalid_parameter_exception * @throws moodle_exception * @param int $useridto the user id who received the message * @return \core_external\external_description */ public static function get_unread_popup_notification_count($useridto) { global $USER; $params = self::validate_parameters( self::get_unread_popup_notification_count_parameters(), array('useridto' => $useridto) ); $context = context_system::instance(); self::validate_context($context); $useridto = $params['useridto']; if (!empty($useridto)) { if (core_user::is_real_user($useridto)) { $userto = core_user::get_user($useridto, '*', MUST_EXIST); } else { throw new moodle_exception('invaliduser'); } } // Check if the current user is the sender/receiver or just a privileged user. if ($useridto != $USER->id and !has_capability('moodle/site:readallmessages', $context)) { throw new moodle_exception('accessdenied', 'admin'); } return \message_popup\api::count_unread_popup_notifications($useridto); } /** * Get unread popup notification count return description. * * @return external_single_structure * @since 3.2 */ public static function get_unread_popup_notification_count_returns() { return new external_value(PARAM_INT, 'The count of unread popup notifications'); } } PKS\\ ?EEdb/services.phpnu[. defined('MOODLE_INTERNAL') || die(); /** * External functions and service definitions. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $functions = array( 'message_popup_get_popup_notifications' => array( 'classname' => 'message_popup_external', 'methodname' => 'get_popup_notifications', 'classpath' => 'message/output/popup/externallib.php', 'description' => 'Retrieve a list of popup notifications for a user', 'type' => 'read', 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), 'message_popup_get_unread_popup_notification_count' => array( 'classname' => 'message_popup_external', 'methodname' => 'get_unread_popup_notification_count', 'classpath' => 'message/output/popup/externallib.php', 'description' => 'Retrieve the count of unread popup notifications for a given user', 'type' => 'read', 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), 'readonlysession' => true, ), ); PKS\iɛdb/install.phpnu[. /** * Installation code for the popup message processor * * @package message_popup * @copyright 2009 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Install the popup message processor */ function xmldb_message_popup_install() { global $DB; $result = true; $provider = new stdClass(); $provider->name = 'popup'; $DB->insert_record('message_processors', $provider); return $result; } PKS\,aadb/upgrade.phpnu[. /** * Upgrade code for popup message processor * * @package message_popup * @copyright 2008 Luis Rodrigues * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ /** * Upgrade code for the popup message processor * * @param int $oldversion The version that we are upgrading from */ function xmldb_message_popup_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PKS\[idb/install.xmlnu[
PKS\Y^*lang/en/message_popup.phpnu[. /** * Strings for component 'message_popup', language 'en' * * @package message_popup * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Web'; $string['privacy:metadata'] = 'The messaging Web plugin does not store any personal data.'; PKS\V V lib.phpnu[. /** * Contains standard functions for message_popup. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Renders the popup. * * @param renderer_base $renderer * @return string The HTML */ function message_popup_render_navbar_output(\renderer_base $renderer) { global $USER, $CFG; // Early bail out conditions. if (!isloggedin() || isguestuser() || \core_user::awaiting_action()) { return ''; } $output = ''; // Add the notifications popover. $enabled = \core_message\api::is_processor_enabled("popup"); if ($enabled) { $unreadcount = \message_popup\api::count_unread_popup_notifications($USER->id); $caneditownmessageprofile = has_capability('moodle/user:editownmessageprofile', context_system::instance()); $preferencesurl = $caneditownmessageprofile ? new moodle_url('/message/notificationpreferences.php') : null; $context = [ 'userid' => $USER->id, 'unreadcount' => $unreadcount, 'urls' => [ 'seeall' => (new moodle_url('/message/output/popup/notifications.php'))->out(), 'preferences' => $preferencesurl ? $preferencesurl->out() : null, ], ]; $output .= $renderer->render_from_template('message_popup/notification_popover', $context); } // Add the messages popover. if (!empty($CFG->messaging)) { $unreadcount = \core_message\api::count_unread_conversations($USER); $requestcount = \core_message\api::get_received_contact_requests_count($USER->id); $context = [ 'userid' => $USER->id, 'unreadcount' => $unreadcount + $requestcount ]; $output .= $renderer->render_from_template('core_message/message_popover', $context); } return $output; } PKS\^Gǔ version.phpnu[. /** * Popup message processor version information * * @package message_popup * @copyright 2008 Luis Rodrigues * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'message_popup'; // Full name of the plugin (used for diagnostics) PKS\89classes/api.phpnu[. /** * Contains class used to return information to display for the message popup. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace message_popup; defined('MOODLE_INTERNAL') || die(); /** * Class used to return information to display for the message popup. * * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Get popup notifications for the specified users. Nothing is returned if notifications are disabled. * * @param int $useridto the user id who received the notification * @param string $sort the column name to order by including optionally direction * @param int $limit limit the number of result returned * @param int $offset offset the result set by this amount * @return array notification records * @throws \moodle_exception * @since 3.2 */ public static function get_popup_notifications($useridto = 0, $sort = 'DESC', $limit = 0, $offset = 0) { global $DB, $USER; $sort = strtoupper($sort); if ($sort != 'DESC' && $sort != 'ASC') { throw new \moodle_exception('invalid parameter: sort: must be "DESC" or "ASC"'); } if (empty($useridto)) { $useridto = $USER->id; } // Is notification enabled ? if ($useridto == $USER->id) { $disabled = $USER->emailstop; } else { $user = \core_user::get_user($useridto, "emailstop", MUST_EXIST); $disabled = $user->emailstop; } if ($disabled) { // Notifications are disabled. return array(); } $sql = "SELECT n.id, n.useridfrom, n.useridto, n.subject, n.fullmessage, n.fullmessageformat, n.fullmessagehtml, n.smallmessage, n.contexturl, n.contexturlname, n.timecreated, n.component, n.eventtype, n.timeread, n.customdata FROM {notifications} n WHERE n.id IN (SELECT notificationid FROM {message_popup_notifications}) AND n.useridto = ? ORDER BY timecreated $sort, timeread $sort, id $sort"; $notifications = []; $records = $DB->get_recordset_sql($sql, [$useridto], $offset, $limit); foreach ($records as $record) { $notifications[] = (object) $record; } $records->close(); return $notifications; } /** * Count the unread notifications for a user. * * @param int $useridto the user id who received the notification * @return int count of the unread notifications * @since 3.2 */ public static function count_unread_popup_notifications($useridto = 0) { global $USER, $DB; if (empty($useridto)) { $useridto = $USER->id; } return $DB->count_records_sql( "SELECT count(id) FROM {notifications} WHERE id IN (SELECT notificationid FROM {message_popup_notifications}) AND useridto = ? AND timeread is NULL", [$useridto] ); } } PKS\ɿcclasses/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for message_popup. * * @package message_popup * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace message_popup\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for message_popup implementing null_provider. * * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PKS\Ӻ7 7 %classes/output/popup_notification.phpnu[. /** * Contains class used to prepare a popup notification for display. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace message_popup\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/message/lib.php'); use renderable; use templatable; use moodle_url; use core_user; /** * Class to prepare a popup notification for display. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class popup_notification implements templatable, renderable { /** * @var \stdClass The notification. */ protected $notification; /** * Constructor. * * @param \stdClass $notification */ public function __construct($notification) { $this->notification = $notification; } public function export_for_template(\renderer_base $output) { $context = clone $this->notification; $context->timecreatedpretty = get_string('ago', 'message', format_time(time() - $context->timecreated)); $context->text = message_format_message_text($context); $context->read = $context->timeread ? true : false; // Need to strip any HTML from these. $context->subject = clean_param($context->subject, PARAM_TEXT); $context->contexturlname = clean_param($context->contexturlname, PARAM_TEXT); $context->shortenedsubject = shorten_text($context->subject, 125); if (!empty($context->component) && substr($context->component, 0, 4) == 'mod_') { $iconurl = $output->image_url('monologo', $context->component); } else { $iconurl = $output->image_url('i/marker', 'core'); } $context->iconurl = $iconurl->out(); return $context; } } PKS\8templates/notification_area_content_area_footer.mustachenu[{{! 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 . }} {{! @template message_popup/notification_area_content_area_footer This template will render the footer for the content area in the notification area on the notifications page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * contexturl The URL for the associated resource * contexturlname Human readable name for the URL Example context (json): { } }} {{#str}} viewnotificationresource, message, {{contexturlname}}{{/str}} PKS\)K(8templates/notification_area_content_area_header.mustachenu[{{! 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 . }} {{! @template message_popup/notification_area_content_area_header This template will render the header for the content area in the notification area on the notifications page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * iconurl The URL for the notification icon * subject The notification subject * timecreatedpretty Pretty format when the notification was created Example context (json): { } }}
{{#pix}} e/text_highlight, core, {{#str}} notificationimage, message {{/str}} {{/pix}}
{{subject}}
{{timecreatedpretty}}
PKS\ - ,templates/notification_content_item.mustachenu[{{! 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 . }} {{! @template message_popup/notification_content_item This template will render the notification content item for the navigation bar notification menu. Classes required for JS: * none Data attributes required for JS: * All data attributes are required Context variables required for this template: * contexturl A link to the notification resource * read If the notification is read or not * subject The subject text * id Notification id * iconurl The URL for the notification icon * shortenedsubject A shortened version of subject text * timecreatedpretty Pretty formatted time stamp * viewmoreurl The link to the full notification Example context (json): { "contexturl": "http://www.moodle.com", "read": true, "subject": "You have a notification", "id": 1, "iconurl": "http://www.moodle.com", "shortenedsubject": "You have a...", "timecreatedpretty": "5 minutes ago", "viewmoreurl": "http://www.moodle.com" } }} {{/contexturl}} PKS\O  $templates/notification_area.mustachenu[{{! 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 . }} {{! @template message_popup/notification_area This template will render the notification area on the notifications page. 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 * offset The offset amound of the notification list * limit The limit amount for the notification list * notificationid The specific notification to select Example context (json): { "userid":3 } }}
{{#str}} nonotifications, message {{/str}}
{{> core/loading }}
{{#str}} selectnotificationtoview, message {{/str}}
{{#js}} require(['jquery', 'message_popup/notification_area_control_area', 'message_popup/notification_area_content_area'], function($, ControlArea, ContentArea) { var userId = {{userid}}; var controlArea = new ControlArea($('[data-region="control-area"]'), userId); {{#offset}} controlArea.setOffset({{.}}); {{/offset}} {{#limit}} controlArea.setLimit({{.}}); {{/limit}} {{#notificationid}} controlArea.loadMoreNotifications().done(function() { controlArea.showNotification({{.}}); }); {{/notificationid}} {{^notificationid}} controlArea.loadMoreNotifications(); {{/notificationid}} new ContentArea($('[data-region="content-area"]'), userId); }); {{/js}} PKS\XDö 'templates/message_content_item.mustachenu[{{! 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 . }} {{! @template message_popup/message_content_item This template will render the message content item for the navigation bar message menu. Classes required for JS: * none Data attributes required for JS: * All data attributes are required Context variables required for this template: * isread If the message is read or not * contexturl The link to the message on the messages page * fullname The name of the sender * profileimageurl The URL for the sender's profile image * sentfromcurrentuser Was the last message sent by the current user * lastmessage The message text * unreadcount The number of unread messages in this conversation Example context (json): { "isread": true, "contexturl": "http://www.moodle.com", "fullname": "Some Person", "profileimageurl": "http://www.moodle.com", "sentfromcurrentuser": false, "lastmessage": "Hello, this is Some Person!", "unreadcount": 1 } }}

{{fullname}}

{{#sentfromcurrentuser}} {{#str}}you, message{{/str}} {{/sentfromcurrentuser}} {{lastmessage}}

{{unreadcount}}
PKS\0 'templates/notification_popover.mustachenu[{{! 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 . }} {{! @template message_popup/notification_popover This template will render the notification 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-notifications{{/classes}} {{$attributes}}id="nav-notification-popover-container" data-userid="{{userid}}"{{/attributes}} {{$togglelabel}}{{! }}{{^unreadcount}} {{#str}} shownotificationwindownonew, message {{/str}} {{/unreadcount}} {{! }}{{#unreadcount}} {{#str}} shownotificationwindowwithcount, message, {{.}} {{/str}} {{/unreadcount}} {{! }}{{/togglelabel}} {{$togglecontent}} {{#pix}} i/notifications, core {{/pix}} {{/togglecontent}} {{$containerrole}}dialog{{/containerrole}} {{$containerattributes}} aria-modal="true" tabindex="-1" {{/containerattributes}} {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}} {{$headertext}}{{#str}} notifications, message {{/str}}{{/headertext}} {{$headeractions}} {{#pix}} t/markasread, core {{/pix}} {{> core/loading }} {{! Records notifications being marked as read state message. }} {{# urls.preferences }} {{#pix}} i/settings, core {{/pix}} {{/ urls.preferences }} {{/headeractions}} {{$content}}
{{#str}} nonotifications, message {{/str}}
{{/content}} {{/ core/popover_region }} {{#js}} require(['jquery', 'message_popup/notification_popover_controller'], function($, Controller) { var container = $('#nav-notification-popover-container'); var controller = new Controller(container); controller.registerEventListeners(); controller.registerListNavigationEventListeners(); }); {{/js}} PKS\/9templates/notification_area_content_area_content.mustachenu[{{! 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 . }} {{! @template message_popup/notification_area_content_area_content This template will render the content for the content area in the notification area on the notifications page. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * fullmessagehtml The notification in HTML format * fullmessage The notification in plain text (if no HTML) Example context (json): { } }} {{#fullmessagehtml}}{{{.}}}{{/fullmessagehtml}} {{^fullmessagehtml}}
{{fullmessage}}
{{/fullmessagehtml}} PKS\HT  "amd/src/notification_repository.jsnu[// 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 . /** * Retrieves notifications from the server. * * @module message_popup/notification_repository * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['core/ajax', 'core/notification'], function(Ajax, Notification) { /** * Retrieve a list of notifications from the server. * * @param {object} args The request arguments * @return {object} jQuery promise */ var query = function(args) { if (typeof args.limit === 'undefined') { args.limit = 20; } if (typeof args.offset === 'undefined') { args.offset = 0; } var request = { methodname: 'message_popup_get_popup_notifications', args: args }; var promise = Ajax.call([request])[0]; promise.fail(Notification.exception); return promise; }; /** * Get the number of unread notifications from the server. * * @param {object} args The request arguments * @return {object} jQuery promise */ var countUnread = function(args) { var request = { methodname: 'message_popup_get_unread_popup_notification_count', args: args }; var promise = Ajax.call([request])[0]; promise.fail(Notification.exception); return promise; }; /** * Mark all notifications for the given user as read. * * @param {object} args The request arguments: * @return {object} jQuery promise */ var markAllAsRead = function(args) { var request = { methodname: 'core_message_mark_all_notifications_as_read', args: args }; var promise = Ajax.call([request])[0]; promise.fail(Notification.exception); return promise; }; /** * Mark a specific notification as read. * * @param {int} id The notification id * @param {int} timeread The read timestamp (optional) * @return {object} jQuery promise */ var markAsRead = function(id, timeread) { var args = { notificationid: id, }; if (timeread) { args.timeread = timeread; } var request = { methodname: 'core_message_mark_notification_read', args: args }; var promise = Ajax.call([request])[0]; promise.fail(Notification.exception); return promise; }; return { query: query, countUnread: countUnread, markAllAsRead: markAllAsRead, markAsRead: markAsRead, }; }); PKS\ÎAA*amd/src/notification_popover_controller.jsnu[// 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 . /** * Controls the notification popover in the nav bar. * * See template: message_popup/notification_popover * * @module message_popup/notification_popover_controller * @class notification_popover_controller * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url', 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller', 'message_popup/notification_repository', 'message_popup/notification_area_events', 'core/local/aria/focuslock', ], function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents, PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) { var SELECTORS = { MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]', ALL_NOTIFICATIONS_CONTAINER: '[data-region="all-notifications"]', NOTIFICATION: '[data-region="notification-content-item-container"]', UNREAD_NOTIFICATION: '[data-region="notification-content-item-container"].unread', NOTIFICATION_LINK: '[data-action="content-item-link"]', EMPTY_MESSAGE: '[data-region="empty-message"]', COUNT_CONTAINER: '[data-region="count-container"]', NOTIFICATION_READ_FEEDBACK: '[data-region="notification-read-feedback"]', CLOSE_NOTIFICATION_POPOVER: '[data-action="close-notification-popover"]', }; /** * Constructor for the NotificationPopoverController. * Extends PopoverRegionController. * * @param {object} element jQuery object root element of the popover */ var NotificationPopoverController = function(element) { // Initialise base class. PopoverController.call(this, element); this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON); this.unreadCount = 0; this.lastQueried = 0; this.userId = this.root.attr('data-userid'); this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER); this.limit = 20; this.offset = 0; this.loadedAll = false; this.initialLoad = false; // Let's find out how many unread notifications there are. this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html(); }; /** * Clone the parent prototype. */ NotificationPopoverController.prototype = Object.create(PopoverController.prototype); /** * Make sure the constructor is set correctly. */ NotificationPopoverController.prototype.constructor = NotificationPopoverController; /** * 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 notifications. * * @method updateButtonAriaLabel */ NotificationPopoverController.prototype.updateButtonAriaLabel = function() { if (this.isMenuOpen()) { Str.get_string('hidenotificationwindow', 'message').done(function(string) { this.menuToggle.attr('aria-label', string); }.bind(this)); } else { if (this.unreadCount) { Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) { this.menuToggle.attr('aria-label', string); }.bind(this)); } else { Str.get_string('shownotificationwindownonew', 'message').done(function(string) { this.menuToggle.attr('aria-label', string); }.bind(this)); } } }; /** * Return the jQuery element with the content. This will return either * the unread notification container or the all notification container * depending on which is currently visible. * * @method getContent * @return {object} jQuery object currently visible content contianer */ NotificationPopoverController.prototype.getContent = function() { return this.container; }; /** * Get the offset value for the current state of the popover in order * to sent to the backend to correctly paginate the notifications. * * @method getOffset * @return {int} current offset */ NotificationPopoverController.prototype.getOffset = function() { return this.offset; }; /** * Increment the offset for the current state, if required. * * @method incrementOffset */ NotificationPopoverController.prototype.incrementOffset = function() { this.offset += this.limit; }; /** * Check if the first load of notification has been triggered for the current * state of the popover. * * @method hasDoneInitialLoad * @return {bool} true if first notification loaded, false otherwise */ NotificationPopoverController.prototype.hasDoneInitialLoad = function() { return this.initialLoad; }; /** * Check if we've loaded all of the notifications for the current popover * state. * * @method hasLoadedAllContent * @return {bool} true if all notifications loaded, false otherwise */ NotificationPopoverController.prototype.hasLoadedAllContent = function() { return this.loadedAll; }; /** * Set the state of the loaded all content property for the current state * of the popover. * * @method setLoadedAllContent * @param {bool} val True if all content is loaded, false otherwise */ NotificationPopoverController.prototype.setLoadedAllContent = function(val) { this.loadedAll = val; }; /** * Show the unread notification count badge on the menu toggle if there * are unread notifications, otherwise hide it. * * @method renderUnreadCount */ NotificationPopoverController.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 notification count badge on the menu toggle. * * @method hideUnreadCount */ NotificationPopoverController.prototype.hideUnreadCount = function() { this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden'); }; /** * Find the notification element for the given id. * * @param {int} id * @method getNotificationElement * @return {object|null} The notification element */ NotificationPopoverController.prototype.getNotificationElement = function(id) { var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]'); return element.length == 1 ? element : null; }; /** * Render the notification data with the appropriate template and add it to the DOM. * * @method renderNotifications * @param {array} notifications Notification data * @param {object} container jQuery object the container to append the rendered notifications * @return {object} jQuery promise that is resolved when all notifications have been * rendered and added to the DOM */ NotificationPopoverController.prototype.renderNotifications = function(notifications, container) { var promises = []; $.each(notifications, function(index, notification) { // Determine what the offset was when loading this notification. var offset = this.getOffset() - this.limit; // Update the view more url to contain the offset to allow the notifications // page to load to the correct position in the list of notifications. notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', { notificationid: notification.id, offset: offset, }); // Link to mark read page before loading the actual link. var notificationurlparams = { notificationid: notification.id }; notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams); var promise = Templates.render('message_popup/notification_content_item', notification) .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 notifications from the server, if we aren't already * loading some and haven't already loaded all of them. * * Takes into account the current mode of the popover and will request only * unread notifications if required. * * All notifications are marked as read by the server when they are returned. * * @method loadMoreNotifications * @return {object} jQuery promise that is resolved when notifications have been * retrieved and added to the DOM */ NotificationPopoverController.prototype.loadMoreNotifications = function() { if (this.isLoading || this.hasLoadedAllContent()) { return $.Deferred().resolve(); } this.startLoading(); var request = { limit: this.limit, offset: this.getOffset(), useridto: this.userId, }; var container = this.getContent(); return NotificationRepo.query(request).then(function(result) { var notifications = result.notifications; this.unreadCount = result.unreadcount; this.lastQueried = Math.floor(new Date().getTime() / 1000); this.setLoadedAllContent(!notifications.length || notifications.length < this.limit); this.initialLoad = true; this.updateButtonAriaLabel(); if (notifications.length) { this.incrementOffset(); return this.renderNotifications(notifications, container); } return false; }.bind(this)) .always(function() { this.stopLoading(); }.bind(this)); }; /** * Send a request to the server to mark all unread notifications as read and update * the unread count and unread notification elements appropriately. * * @return {Promise} * @method markAllAsRead */ NotificationPopoverController.prototype.markAllAsRead = function() { this.markAllReadButton.addClass('loading'); var request = { useridto: this.userId, timecreatedto: this.lastQueried, }; return NotificationRepo.markAllAsRead(request) .then(function() { this.unreadCount = 0; this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread'); // Set the ARIA live region's contents with the feedback. const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK); Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => { readFeedback.innerHTML = notificationsmarkedasread; }); }.bind(this)) .always(function() { this.markAllReadButton.removeClass('loading'); }.bind(this)); }; /** * Add all of the required event listeners for this notification popover. * * @method registerEventListeners */ NotificationPopoverController.prototype.registerEventListeners = function() { CustomEvents.define(this.root, [ CustomEvents.events.activate, ]); // Mark all notifications read if the user activates the mark all as read button. this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) { if (this.unreadCount > 0) { this.markAllAsRead(); } e.stopPropagation(); data.originalEvent.preventDefault(); }.bind(this)); // Mark individual notification read if the user activates it. this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) { var element = $(e.target).closest(SELECTORS.NOTIFICATION); if (element.hasClass('unread')) { this.unreadCount--; element.removeClass('unread'); } e.stopPropagation(); }.bind(this)); this.root.on(CustomEvents.events.activate, SELECTORS.CLOSE_NOTIFICATION_POPOVER, function(e) { e.preventDefault(); $(this.root).trigger(CustomEvents.events.escape); e.stopPropagation(); }.bind(this)); // Update the notification information when the menu is opened. this.root.on(this.events().menuOpened, function() { this.hideUnreadCount(); this.updateButtonAriaLabel(); if (!this.hasDoneInitialLoad()) { this.loadMoreNotifications(); } // Lock focus to the popover when it is opened, it is the parent of the container. const contentContainer = this.getContentContainer()[0].parentNode; FocusLock.trapFocus(contentContainer); }.bind(this)); // Update the unread notification count when the menu is closed. this.root.on(this.events().menuClosed, function() { this.renderUnreadCount(); this.updateButtonAriaLabel(); // Lock focus to the popover when it is opened, it is the parent of the container. FocusLock.untrapFocus(); }.bind(this)); // Set aria attributes when popover is loading. this.root.on(this.events().startLoading, function() { this.getContent().attr('aria-busy', 'true'); }.bind(this)); // Set aria attributes when popover is finished loading. this.root.on(this.events().stopLoading, function() { this.getContent().attr('aria-busy', 'false'); }.bind(this)); // Load more notifications if the user has scrolled to the end of content // item list. this.getContentContainer().on(CustomEvents.events.scrollBottom, function() { if (!this.isLoading && !this.hasLoadedAllContent()) { this.loadMoreNotifications(); } }.bind(this)); // Stop mouse scroll from propagating to the window element and // scrolling the page. CustomEvents.define(this.getContentContainer(), [ CustomEvents.events.scrollLock ]); // Listen for when a notification is shown in the notifications page and mark // it as read, if it's unread. $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) { if (!notification.read) { var element = this.getNotificationElement(notification.id); if (element) { element.removeClass('unread'); } this.unreadCount--; this.renderUnreadCount(); } }.bind(this)); }; return NotificationPopoverController; }); PKS\x)amd/src/notification_area_content_area.jsnu[// 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 . /** * Controls the content area of the notification area on the * notification page. * * @module message_popup/notification_area_content_area * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events', 'message_popup/notification_repository', 'message_popup/notification_area_events'], function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) { var SELECTORS = { CONTAINER: '[data-region="notification-area"]', CONTENT: '[data-region="content"]', HEADER: '[data-region="header"]', FOOTER: '[data-region="footer"]', TOGGLE_MODE: '[data-action="toggle-mode"]', }; var TEMPLATES = { HEADER: 'message_popup/notification_area_content_area_header', CONTENT: 'message_popup/notification_area_content_area_content', FOOTER: 'message_popup/notification_area_content_area_footer', }; /** * Constructor for the ContentArea * * @class * @param {object} root The root element for the content area * @param {int} userId The user id of the current user */ var ContentArea = function(root, userId) { this.root = $(root); this.container = this.root.closest(SELECTORS.CONTAINER); this.userId = userId; this.header = this.root.find(SELECTORS.HEADER); this.content = this.root.find(SELECTORS.CONTENT); this.footer = this.root.find(SELECTORS.FOOTER); this.registerEventListeners(); }; /** * Get the root element. * * @method getRoot * @return {object} jQuery element */ ContentArea.prototype.getRoot = function() { return this.root; }; /** * Get the container element (which the content area is within). * * @method getContainer * @return {object} jQuery element */ ContentArea.prototype.getContainer = function() { return this.container; }; /** * Get the user id. * * @method getUserId * @return {int} */ ContentArea.prototype.getUserId = function() { return this.userId; }; /** * Get the content area header element. * * @method getHeader * @return {object} jQuery element */ ContentArea.prototype.getHeader = function() { return this.header; }; /** * Get the content area content element. * * @method getContent * @return {object} jQuery element */ ContentArea.prototype.getContent = function() { return this.content; }; /** * Get the content area footer element. * * @method getFooter * @return {object} jQuery element */ ContentArea.prototype.getFooter = function() { return this.footer; }; /** * Display the content area. Typically used with responsive * styling on smaller screens. * * @method show */ ContentArea.prototype.show = function() { this.getContainer().addClass('show-content-area'); }; /** * Hide the content area. Typically used with responsive * styling on smaller screens. * * @method hide */ ContentArea.prototype.hide = function() { this.getContainer().removeClass('show-content-area'); }; /** * Change the HTML in the content area header element. * * @method setHeaderHTML * @param {string} html The HTML to be set */ ContentArea.prototype.setHeaderHTML = function(html) { this.getHeader().empty().html(html); }; /** * Change the HTML in the content area content element. * * @method setContentHMTL * @param {string} html The HTML to be set. */ ContentArea.prototype.setContentHTML = function(html) { this.getContent().empty().html(html); }; /** * Change the HTML in the content area footer element. * * @method setFooterHTML * @param {string} html The HTML to be set. */ ContentArea.prototype.setFooterHTML = function(html) { this.getFooter().empty().html(html); }; /** * Render the given notification context in the content area. * * @method showNotification * @param {object} notification The notification context (from a webservice) * @return {object} jQuery promise */ ContentArea.prototype.showNotification = function(notification) { var headerPromise = Templates.render(TEMPLATES.HEADER, notification).done(function(html) { this.setHeaderHTML(html); }.bind(this)); var contentPromise = Templates.render(TEMPLATES.CONTENT, notification).done(function(html) { this.setContentHTML(html); }.bind(this)); var footerPromise = Templates.render(TEMPLATES.FOOTER, notification).done(function(html) { this.setFooterHTML(html); }.bind(this)); return $.when(headerPromise, contentPromise, footerPromise).done(function() { this.show(); this.getContainer().trigger(NotificationAreaEvents.notificationShown, [notification]); }.bind(this)); }; /** * Create the event listeners for the content area. * * @method registerEventListeners */ ContentArea.prototype.registerEventListeners = function() { CustomEvents.define(this.getRoot(), [ CustomEvents.events.activate ]); this.getRoot().on(CustomEvents.events.activate, SELECTORS.VIEW_TOGGLE, function() { this.hide(); }.bind(this)); this.getContainer().on(NotificationAreaEvents.showNotification, function(e, notification) { this.showNotification(notification); }.bind(this)); }; return ContentArea; }); PKS\bb#amd/src/notification_area_events.jsnu[// 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 . /** * Contain the events that can be fired in the notification area on * the notifications page. * * @module message_popup/notification_area_events * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { showNotification: 'notification-area-events:showNotification', notificationShown: 'notification-area-events:notificationShown', }; PKS\;5::)amd/src/notification_area_control_area.jsnu[// 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 . /** * Controls the notification area on the notification page. * * @module message_popup/notification_area_control_area * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events', 'message_popup/notification_repository', 'message_popup/notification_area_events'], function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) { var SELECTORS = { CONTAINER: '[data-region="notification-area"]', CONTENT: '[data-region="content"]', NOTIFICATION: '[data-region="notification-content-item-container"]', CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', }; var TEMPLATES = { NOTIFICATION: 'message_popup/notification_content_item', }; /** * Constructor for ControlArea * * @class * @param {object} root The root element for the content area * @param {int} userId The user id of the current user */ var ControlArea = function(root, userId) { this.root = $(root); this.container = this.root.closest(SELECTORS.CONTAINER); this.userId = userId; this.content = this.root.find(SELECTORS.CONTENT); this.offset = 0; this.limit = 20; this.initialLoad = false; this.isLoading = false; this.loadedAll = false; this.notifications = {}; this.registerEventListeners(); }; /** * Get the root element. * * @method getRoot * @return {object} jQuery element */ ControlArea.prototype.getRoot = function() { return this.root; }; /** * Get the container element (which the control area is within). * * @method getContainer * @return {object} jQuery element */ ControlArea.prototype.getContainer = function() { return this.container; }; /** * Get the user id. * * @method getUserId * @return {int} */ ControlArea.prototype.getUserId = function() { return this.userId; }; /** * Get the control area content element. * * @method getContent * @return {object} jQuery element */ ControlArea.prototype.getContent = function() { return this.content; }; /** * Get the offset value for paginated loading of the * notifications. * * @method getOffset * @return {int} */ ControlArea.prototype.getOffset = function() { return this.offset; }; /** * Get the limit value for the paginated loading of the * notifications. * * @method getLimit * @return {int} */ ControlArea.prototype.getLimit = function() { return this.limit; }; /** * Set the offset value for the paginated loading of the * notifications. * * @method setOffset * @param {int} value The new offset value */ ControlArea.prototype.setOffset = function(value) { this.offset = value; }; /** * Set the limit value for the paginated loading of the * notifications. * * @method setLimit * @param {int} value The new limit value */ ControlArea.prototype.setLimit = function(value) { this.limit = value; }; /** * Increment the offset by the limit amount. * * @method incrementOffset */ ControlArea.prototype.incrementOffset = function() { this.offset += this.limit; }; /** * Flag the control area as loading. * * @method startLoading */ ControlArea.prototype.startLoading = function() { this.isLoading = true; this.getRoot().addClass('loading'); }; /** * Remove the loading flag from the control area. * * @method stopLoading */ ControlArea.prototype.stopLoading = function() { this.isLoading = false; this.getRoot().removeClass('loading'); }; /** * Check if the first load of notifications has been triggered. * * @method hasDoneInitialLoad * @return {bool} true if first notification loaded, false otherwise */ ControlArea.prototype.hasDoneInitialLoad = function() { return this.initialLoad; }; /** * Check if all of the notifications have been loaded. * * @method hasLoadedAllContent * @return {bool} */ ControlArea.prototype.hasLoadedAllContent = function() { return this.loadedAll; }; /** * Set the state of the loaded all content property. * * @method setLoadedAllContent * @param {bool} val True if all content is loaded, false otherwise */ ControlArea.prototype.setLoadedAllContent = function(val) { this.loadedAll = val; }; /** * Save a notification in the cache. * * @method setCacheNotification * @param {object} notification A notification returned by a webservice */ ControlArea.prototype.setCacheNotification = function(notification) { this.notifications[notification.id] = notification; }; /** * Retrieve a notification from the cache. * * @method getCacheNotification * @param {int} id The id for the notification you wish to retrieve * @return {object} A notification (as returned by a webservice) */ ControlArea.prototype.getCacheNotification = function(id) { return this.notifications[id]; }; /** * Find the notification element in the control area for the given id. * * @method getNotificationElement * @param {int} id The notification id * @return {(object|null)} jQuery element or null */ ControlArea.prototype.getNotificationElement = function(id) { var element = this.getRoot().find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]'); return element.length == 1 ? element : null; }; /** * Scroll the notification element into view within the control area, if it * isn't already visible. * * @method scrollNotificationIntoView * @param {object} notificationElement The jQuery notification element */ ControlArea.prototype.scrollNotificationIntoView = function(notificationElement) { var position = notificationElement.position(); var container = this.getRoot(); var relativeTop = position.top - container.scrollTop(); // If the element isn't in the view window. if (relativeTop > container.innerHeight()) { var height = notificationElement.outerHeight(); // Offset enough to make sure the notification will be in view. height = height * 4; var scrollTo = position.top - height; container.scrollTop(scrollTo); } }; /** * Show the full notification for the given notification element. The notification * context is retrieved from the cache and send as data with an event to be * rendered in the content area. * * @method showNotification * @param {(int|object)} notificationElement The notification id or jQuery notification element */ ControlArea.prototype.showNotification = function(notificationElement) { if (typeof notificationElement !== 'object') { // Assume it's an ID if it's not an object. notificationElement = this.getNotificationElement(notificationElement); } if (notificationElement && notificationElement.length) { this.getRoot().find(SELECTORS.NOTIFICATION).removeClass('selected'); notificationElement.addClass('selected').find(SELECTORS.CAN_RECEIVE_FOCUS).focus(); var notificationId = notificationElement.attr('data-id'); var notification = this.getCacheNotification(notificationId); this.scrollNotificationIntoView(notificationElement); // Create a new version of the notification to send with the notification so // this copy isn't modified. this.getContainer().trigger(NotificationAreaEvents.showNotification, [$.extend({}, notification)]); } }; /** * Send a request to mark the notification as read in the server and remove the unread * status from the element. * * @method markNotificationAsRead * @param {object} notificationElement The jQuery notification element * @return {object} jQuery promise */ ControlArea.prototype.markNotificationAsRead = function(notificationElement) { return NotificationRepo.markAsRead(notificationElement.attr('data-id')).done(function() { notificationElement.removeClass('unread'); }); }; /** * Render the notification data with the appropriate template and add it to the DOM. * * @method renderNotifications * @param {array} notifications Array of notification data * @return {object} jQuery promise that is resolved when all notifications have been * rendered and added to the DOM */ ControlArea.prototype.renderNotifications = function(notifications) { var promises = []; var container = this.getContent(); $.each(notifications, function(index, notification) { // Need to remove the contexturl so the item isn't rendered // as a link. var contextUrl = notification.contexturl; delete notification.contexturl; var promise = Templates.render(TEMPLATES.NOTIFICATION, notification) .then(function(html, js) { // Restore it for the cache. notification.contexturl = contextUrl; this.setCacheNotification(notification); // Pass the Rendered content out. return {html: html, js: js}; }.bind(this)); 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; }); }; /** * Load notifications from the server and render them. * * @method loadMoreNotifications * @return {object} jQuery promise */ ControlArea.prototype.loadMoreNotifications = function() { if (this.isLoading || this.hasLoadedAllContent()) { return $.Deferred().resolve(); } this.startLoading(); var request = { limit: this.getLimit(), offset: this.getOffset(), useridto: this.getUserId(), }; if (!this.initialLoad) { // If this is the first load we may have been given a non-zero offset, // in which case we need to load all notifications preceeding that offset // to make sure the full list is rendered. request.limit = this.getOffset() + this.getLimit(); request.offset = 0; } var promise = NotificationRepo.query(request).then(function(result) { var notifications = result.notifications; this.unreadCount = result.unreadcount; this.setLoadedAllContent(!notifications.length || notifications.length < this.getLimit()); this.initialLoad = true; if (notifications.length) { this.incrementOffset(); return this.renderNotifications(notifications); } return false; }.bind(this)) .always(function() { this.stopLoading(); }.bind(this)); return promise; }; /** * Create the event listeners for the control area. * * @method registerEventListeners */ ControlArea.prototype.registerEventListeners = function() { CustomEvents.define(this.getRoot(), [ CustomEvents.events.activate, CustomEvents.events.scrollBottom, CustomEvents.events.scrollLock, CustomEvents.events.up, CustomEvents.events.down, ]); this.getRoot().on(CustomEvents.events.scrollBottom, function() { this.loadMoreNotifications(); }.bind(this)); this.getRoot().on(CustomEvents.events.activate, SELECTORS.NOTIFICATION, function(e) { var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION); this.showNotification(notificationElement); }.bind(this)); // Show the previous notification in the list. this.getRoot().on(CustomEvents.events.up, SELECTORS.NOTIFICATION, function(e, data) { var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION); this.showNotification(notificationElement.prev()); data.originalEvent.preventDefault(); }.bind(this)); // Show the next notification in the list. this.getRoot().on(CustomEvents.events.down, SELECTORS.NOTIFICATION, function(e, data) { var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION); this.showNotification(notificationElement.next()); data.originalEvent.preventDefault(); }.bind(this)); this.getContainer().on(NotificationAreaEvents.notificationShown, function(e, notification) { if (!notification.read) { var element = this.getNotificationElement(notification.id); if (element) { this.markNotificationAsRead(element); } var cachedNotification = this.getCacheNotification(notification.id); if (cachedNotification) { cachedNotification.read = true; } } }.bind(this)); }; return ControlArea; }); PKS\B,amd/build/notification_repository.min.js.mapnu[{"version":3,"file":"notification_repository.min.js","sources":["../src/notification_repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Retrieves notifications from the server.\n *\n * @module message_popup/notification_repository\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/ajax', 'core/notification'], function(Ajax, Notification) {\n /**\n * Retrieve a list of notifications from the server.\n *\n * @param {object} args The request arguments\n * @return {object} jQuery promise\n */\n var query = function(args) {\n if (typeof args.limit === 'undefined') {\n args.limit = 20;\n }\n\n if (typeof args.offset === 'undefined') {\n args.offset = 0;\n }\n\n var request = {\n methodname: 'message_popup_get_popup_notifications',\n args: args\n };\n\n var promise = Ajax.call([request])[0];\n\n promise.fail(Notification.exception);\n\n return promise;\n };\n\n /**\n * Get the number of unread notifications from the server.\n *\n * @param {object} args The request arguments\n * @return {object} jQuery promise\n */\n var countUnread = function(args) {\n var request = {\n methodname: 'message_popup_get_unread_popup_notification_count',\n args: args\n };\n\n var promise = Ajax.call([request])[0];\n\n promise.fail(Notification.exception);\n\n return promise;\n };\n\n /**\n * Mark all notifications for the given user as read.\n *\n * @param {object} args The request arguments:\n * @return {object} jQuery promise\n */\n var markAllAsRead = function(args) {\n var request = {\n methodname: 'core_message_mark_all_notifications_as_read',\n args: args\n };\n\n var promise = Ajax.call([request])[0];\n\n promise.fail(Notification.exception);\n\n return promise;\n };\n\n /**\n * Mark a specific notification as read.\n *\n * @param {int} id The notification id\n * @param {int} timeread The read timestamp (optional)\n * @return {object} jQuery promise\n */\n var markAsRead = function(id, timeread) {\n var args = {\n notificationid: id,\n };\n\n if (timeread) {\n args.timeread = timeread;\n }\n\n var request = {\n methodname: 'core_message_mark_notification_read',\n args: args\n };\n\n var promise = Ajax.call([request])[0];\n\n promise.fail(Notification.exception);\n\n return promise;\n };\n\n return {\n query: query,\n countUnread: countUnread,\n markAllAsRead: markAllAsRead,\n markAsRead: markAsRead,\n };\n});\n"],"names":["define","Ajax","Notification","query","args","limit","offset","request","methodname","promise","call","fail","exception","countUnread","markAllAsRead","markAsRead","id","timeread","notificationid"],"mappings":";;;;;;;AAsBAA,+CAAO,CAAC,YAAa,sBAAsB,SAASC,KAAMC,oBA8F/C,CACHC,MAxFQ,SAASC,WACS,IAAfA,KAAKC,QACZD,KAAKC,MAAQ,SAGU,IAAhBD,KAAKE,SACZF,KAAKE,OAAS,OAGdC,QAAU,CACVC,WAAY,wCACZJ,KAAMA,MAGNK,QAAUR,KAAKS,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKT,aAAaU,WAEnBH,SAuEPI,YA9Dc,SAAST,UACnBG,QAAU,CACVC,WAAY,oDACZJ,KAAMA,MAGNK,QAAUR,KAAKS,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKT,aAAaU,WAEnBH,SAqDPK,cA5CgB,SAASV,UACrBG,QAAU,CACVC,WAAY,8CACZJ,KAAMA,MAGNK,QAAUR,KAAKS,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKT,aAAaU,WAEnBH,SAmCPM,WAzBa,SAASC,GAAIC,cACtBb,KAAO,CACPc,eAAgBF,IAGhBC,WACAb,KAAKa,SAAWA,cAGhBV,QAAU,CACVC,WAAY,sCACZJ,KAAMA,MAGNK,QAAUR,KAAKS,KAAK,CAACH,UAAU,UAEnCE,QAAQE,KAAKT,aAAaU,WAEnBH"}PKS\5؋)amd/build/notification_area_events.min.jsnu[define("message_popup/notification_area_events",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={showNotification:"notification-area-events:showNotification",notificationShown:"notification-area-events:notificationShown"},_exports.default})); //# sourceMappingURL=notification_area_events.min.js.mapPKS\w w /amd/build/notification_area_content_area.min.jsnu[/** * Controls the content area of the notification area on the * notification page. * * @module message_popup/notification_area_content_area * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("message_popup/notification_area_content_area",["jquery","core/templates","core/notification","core/custom_interaction_events","message_popup/notification_repository","message_popup/notification_area_events"],(function($,Templates,DebugNotification,CustomEvents,NotificationRepo,NotificationAreaEvents){var SELECTORS={CONTAINER:'[data-region="notification-area"]',CONTENT:'[data-region="content"]',HEADER:'[data-region="header"]',FOOTER:'[data-region="footer"]',TOGGLE_MODE:'[data-action="toggle-mode"]'},TEMPLATES_HEADER="message_popup/notification_area_content_area_header",TEMPLATES_CONTENT="message_popup/notification_area_content_area_content",TEMPLATES_FOOTER="message_popup/notification_area_content_area_footer",ContentArea=function(root,userId){this.root=$(root),this.container=this.root.closest(SELECTORS.CONTAINER),this.userId=userId,this.header=this.root.find(SELECTORS.HEADER),this.content=this.root.find(SELECTORS.CONTENT),this.footer=this.root.find(SELECTORS.FOOTER),this.registerEventListeners()};return ContentArea.prototype.getRoot=function(){return this.root},ContentArea.prototype.getContainer=function(){return this.container},ContentArea.prototype.getUserId=function(){return this.userId},ContentArea.prototype.getHeader=function(){return this.header},ContentArea.prototype.getContent=function(){return this.content},ContentArea.prototype.getFooter=function(){return this.footer},ContentArea.prototype.show=function(){this.getContainer().addClass("show-content-area")},ContentArea.prototype.hide=function(){this.getContainer().removeClass("show-content-area")},ContentArea.prototype.setHeaderHTML=function(html){this.getHeader().empty().html(html)},ContentArea.prototype.setContentHTML=function(html){this.getContent().empty().html(html)},ContentArea.prototype.setFooterHTML=function(html){this.getFooter().empty().html(html)},ContentArea.prototype.showNotification=function(notification){var headerPromise=Templates.render(TEMPLATES_HEADER,notification).done(function(html){this.setHeaderHTML(html)}.bind(this)),contentPromise=Templates.render(TEMPLATES_CONTENT,notification).done(function(html){this.setContentHTML(html)}.bind(this)),footerPromise=Templates.render(TEMPLATES_FOOTER,notification).done(function(html){this.setFooterHTML(html)}.bind(this));return $.when(headerPromise,contentPromise,footerPromise).done(function(){this.show(),this.getContainer().trigger(NotificationAreaEvents.notificationShown,[notification])}.bind(this))},ContentArea.prototype.registerEventListeners=function(){CustomEvents.define(this.getRoot(),[CustomEvents.events.activate]),this.getRoot().on(CustomEvents.events.activate,SELECTORS.VIEW_TOGGLE,function(){this.hide()}.bind(this)),this.getContainer().on(NotificationAreaEvents.showNotification,function(e,notification){this.showNotification(notification)}.bind(this))},ContentArea})); //# sourceMappingURL=notification_area_content_area.min.js.mapPKS\@[[-amd/build/notification_area_events.min.js.mapnu[{"version":3,"file":"notification_area_events.min.js","sources":["../src/notification_area_events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the events that can be fired in the notification area on\n * the notifications page.\n *\n * @module message_popup/notification_area_events\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n showNotification: 'notification-area-events:showNotification',\n notificationShown: 'notification-area-events:notificationShown',\n};\n"],"names":["showNotification","notificationShown"],"mappings":"wLAuBe,CACXA,iBAAkB,4CAClBC,kBAAmB"}PKS\#$$3amd/build/notification_area_content_area.min.js.mapnu[{"version":3,"file":"notification_area_content_area.min.js","sources":["../src/notification_area_content_area.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls the content area of the notification area on the\n * notification page.\n *\n * @module message_popup/notification_area_content_area\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events',\n 'message_popup/notification_repository', 'message_popup/notification_area_events'],\n function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) {\n\n var SELECTORS = {\n CONTAINER: '[data-region=\"notification-area\"]',\n CONTENT: '[data-region=\"content\"]',\n HEADER: '[data-region=\"header\"]',\n FOOTER: '[data-region=\"footer\"]',\n TOGGLE_MODE: '[data-action=\"toggle-mode\"]',\n };\n\n var TEMPLATES = {\n HEADER: 'message_popup/notification_area_content_area_header',\n CONTENT: 'message_popup/notification_area_content_area_content',\n FOOTER: 'message_popup/notification_area_content_area_footer',\n };\n\n /**\n * Constructor for the ContentArea\n *\n * @class\n * @param {object} root The root element for the content area\n * @param {int} userId The user id of the current user\n */\n var ContentArea = function(root, userId) {\n this.root = $(root);\n this.container = this.root.closest(SELECTORS.CONTAINER);\n this.userId = userId;\n this.header = this.root.find(SELECTORS.HEADER);\n this.content = this.root.find(SELECTORS.CONTENT);\n this.footer = this.root.find(SELECTORS.FOOTER);\n\n this.registerEventListeners();\n };\n\n /**\n * Get the root element.\n *\n * @method getRoot\n * @return {object} jQuery element\n */\n ContentArea.prototype.getRoot = function() {\n return this.root;\n };\n\n /**\n * Get the container element (which the content area is within).\n *\n * @method getContainer\n * @return {object} jQuery element\n */\n ContentArea.prototype.getContainer = function() {\n return this.container;\n };\n\n /**\n * Get the user id.\n *\n * @method getUserId\n * @return {int}\n */\n ContentArea.prototype.getUserId = function() {\n return this.userId;\n };\n\n /**\n * Get the content area header element.\n *\n * @method getHeader\n * @return {object} jQuery element\n */\n ContentArea.prototype.getHeader = function() {\n return this.header;\n };\n\n /**\n * Get the content area content element.\n *\n * @method getContent\n * @return {object} jQuery element\n */\n ContentArea.prototype.getContent = function() {\n return this.content;\n };\n\n /**\n * Get the content area footer element.\n *\n * @method getFooter\n * @return {object} jQuery element\n */\n ContentArea.prototype.getFooter = function() {\n return this.footer;\n };\n\n /**\n * Display the content area. Typically used with responsive\n * styling on smaller screens.\n *\n * @method show\n */\n ContentArea.prototype.show = function() {\n this.getContainer().addClass('show-content-area');\n };\n\n /**\n * Hide the content area. Typically used with responsive\n * styling on smaller screens.\n *\n * @method hide\n */\n ContentArea.prototype.hide = function() {\n this.getContainer().removeClass('show-content-area');\n };\n\n /**\n * Change the HTML in the content area header element.\n *\n * @method setHeaderHTML\n * @param {string} html The HTML to be set\n */\n ContentArea.prototype.setHeaderHTML = function(html) {\n this.getHeader().empty().html(html);\n };\n\n /**\n * Change the HTML in the content area content element.\n *\n * @method setContentHMTL\n * @param {string} html The HTML to be set.\n */\n ContentArea.prototype.setContentHTML = function(html) {\n this.getContent().empty().html(html);\n };\n\n /**\n * Change the HTML in the content area footer element.\n *\n * @method setFooterHTML\n * @param {string} html The HTML to be set.\n */\n ContentArea.prototype.setFooterHTML = function(html) {\n this.getFooter().empty().html(html);\n };\n\n /**\n * Render the given notification context in the content area.\n *\n * @method showNotification\n * @param {object} notification The notification context (from a webservice)\n * @return {object} jQuery promise\n */\n ContentArea.prototype.showNotification = function(notification) {\n var headerPromise = Templates.render(TEMPLATES.HEADER, notification).done(function(html) {\n this.setHeaderHTML(html);\n }.bind(this));\n\n var contentPromise = Templates.render(TEMPLATES.CONTENT, notification).done(function(html) {\n this.setContentHTML(html);\n }.bind(this));\n\n var footerPromise = Templates.render(TEMPLATES.FOOTER, notification).done(function(html) {\n this.setFooterHTML(html);\n }.bind(this));\n\n return $.when(headerPromise, contentPromise, footerPromise).done(function() {\n this.show();\n this.getContainer().trigger(NotificationAreaEvents.notificationShown, [notification]);\n }.bind(this));\n };\n\n /**\n * Create the event listeners for the content area.\n *\n * @method registerEventListeners\n */\n ContentArea.prototype.registerEventListeners = function() {\n CustomEvents.define(this.getRoot(), [\n CustomEvents.events.activate\n ]);\n\n this.getRoot().on(CustomEvents.events.activate, SELECTORS.VIEW_TOGGLE, function() {\n this.hide();\n }.bind(this));\n\n this.getContainer().on(NotificationAreaEvents.showNotification, function(e, notification) {\n this.showNotification(notification);\n }.bind(this));\n };\n\n return ContentArea;\n});\n"],"names":["define","$","Templates","DebugNotification","CustomEvents","NotificationRepo","NotificationAreaEvents","SELECTORS","CONTAINER","CONTENT","HEADER","FOOTER","TOGGLE_MODE","TEMPLATES","ContentArea","root","userId","container","this","closest","header","find","content","footer","registerEventListeners","prototype","getRoot","getContainer","getUserId","getHeader","getContent","getFooter","show","addClass","hide","removeClass","setHeaderHTML","html","empty","setContentHTML","setFooterHTML","showNotification","notification","headerPromise","render","done","bind","contentPromise","footerPromise","when","trigger","notificationShown","events","activate","on","VIEW_TOGGLE","e"],"mappings":";;;;;;;;AAuBAA,sDAAO,CAAC,SAAU,iBAAkB,oBAAqB,iCACjD,wCAAyC,2CAC7C,SAASC,EAAGC,UAAWC,kBAAmBC,aAAcC,iBAAkBC,4BAEtEC,UAAY,CACZC,UAAW,oCACXC,QAAS,0BACTC,OAAQ,yBACRC,OAAQ,yBACRC,YAAa,+BAGbC,iBACQ,sDADRA,kBAES,uDAFTA,iBAGQ,sDAURC,YAAc,SAASC,KAAMC,aACxBD,KAAOd,EAAEc,WACTE,UAAYC,KAAKH,KAAKI,QAAQZ,UAAUC,gBACxCQ,OAASA,YACTI,OAASF,KAAKH,KAAKM,KAAKd,UAAUG,aAClCY,QAAUJ,KAAKH,KAAKM,KAAKd,UAAUE,cACnCc,OAASL,KAAKH,KAAKM,KAAKd,UAAUI,aAElCa,iCASTV,YAAYW,UAAUC,QAAU,kBACrBR,KAAKH,MAShBD,YAAYW,UAAUE,aAAe,kBAC1BT,KAAKD,WAShBH,YAAYW,UAAUG,UAAY,kBACvBV,KAAKF,QAShBF,YAAYW,UAAUI,UAAY,kBACvBX,KAAKE,QAShBN,YAAYW,UAAUK,WAAa,kBACxBZ,KAAKI,SAShBR,YAAYW,UAAUM,UAAY,kBACvBb,KAAKK,QAShBT,YAAYW,UAAUO,KAAO,gBACpBL,eAAeM,SAAS,sBASjCnB,YAAYW,UAAUS,KAAO,gBACpBP,eAAeQ,YAAY,sBASpCrB,YAAYW,UAAUW,cAAgB,SAASC,WACtCR,YAAYS,QAAQD,KAAKA,OASlCvB,YAAYW,UAAUc,eAAiB,SAASF,WACvCP,aAAaQ,QAAQD,KAAKA,OASnCvB,YAAYW,UAAUe,cAAgB,SAASH,WACtCN,YAAYO,QAAQD,KAAKA,OAUlCvB,YAAYW,UAAUgB,iBAAmB,SAASC,kBAC1CC,cAAgBzC,UAAU0C,OAAO/B,iBAAkB6B,cAAcG,KAAK,SAASR,WAC1ED,cAAcC,OACrBS,KAAK5B,OAEH6B,eAAiB7C,UAAU0C,OAAO/B,kBAAmB6B,cAAcG,KAAK,SAASR,WAC5EE,eAAeF,OACtBS,KAAK5B,OAEH8B,cAAgB9C,UAAU0C,OAAO/B,iBAAkB6B,cAAcG,KAAK,SAASR,WAC1EG,cAAcH,OACrBS,KAAK5B,cAEAjB,EAAEgD,KAAKN,cAAeI,eAAgBC,eAAeH,KAAK,gBACxDb,YACAL,eAAeuB,QAAQ5C,uBAAuB6C,kBAAmB,CAACT,gBACzEI,KAAK5B,QAQXJ,YAAYW,UAAUD,uBAAyB,WAC3CpB,aAAaJ,OAAOkB,KAAKQ,UAAW,CAChCtB,aAAagD,OAAOC,gBAGnB3B,UAAU4B,GAAGlD,aAAagD,OAAOC,SAAU9C,UAAUgD,YAAa,gBAC9DrB,QACPY,KAAK5B,YAEFS,eAAe2B,GAAGhD,uBAAuBmC,iBAAkB,SAASe,EAAGd,mBACnED,iBAAiBC,eACxBI,KAAK5B,QAGJJ"}PKS\B!!(amd/build/notification_repository.min.jsnu[/** * Retrieves notifications from the server. * * @module message_popup/notification_repository * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("message_popup/notification_repository",["core/ajax","core/notification"],(function(Ajax,Notification){return{query:function(args){void 0===args.limit&&(args.limit=20),void 0===args.offset&&(args.offset=0);var request={methodname:"message_popup_get_popup_notifications",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise},countUnread:function(args){var request={methodname:"message_popup_get_unread_popup_notification_count",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise},markAllAsRead:function(args){var request={methodname:"core_message_mark_all_notifications_as_read",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise},markAsRead:function(id,timeread){var args={notificationid:id};timeread&&(args.timeread=timeread);var request={methodname:"core_message_mark_notification_read",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise}}})); //# sourceMappingURL=notification_repository.min.js.mapPKS\33sPP3amd/build/notification_area_control_area.min.js.mapnu[{"version":3,"file":"notification_area_control_area.min.js","sources":["../src/notification_area_control_area.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls the notification area on the notification page.\n *\n * @module message_popup/notification_area_control_area\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/templates', 'core/notification', 'core/custom_interaction_events',\n 'message_popup/notification_repository', 'message_popup/notification_area_events'],\n function($, Templates, DebugNotification, CustomEvents, NotificationRepo, NotificationAreaEvents) {\n\n var SELECTORS = {\n CONTAINER: '[data-region=\"notification-area\"]',\n CONTENT: '[data-region=\"content\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n CAN_RECEIVE_FOCUS: 'input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]',\n };\n\n var TEMPLATES = {\n NOTIFICATION: 'message_popup/notification_content_item',\n };\n\n /**\n * Constructor for ControlArea\n *\n * @class\n * @param {object} root The root element for the content area\n * @param {int} userId The user id of the current user\n */\n var ControlArea = function(root, userId) {\n this.root = $(root);\n this.container = this.root.closest(SELECTORS.CONTAINER);\n this.userId = userId;\n this.content = this.root.find(SELECTORS.CONTENT);\n this.offset = 0;\n this.limit = 20;\n this.initialLoad = false;\n this.isLoading = false;\n this.loadedAll = false;\n this.notifications = {};\n\n this.registerEventListeners();\n };\n\n /**\n * Get the root element.\n *\n * @method getRoot\n * @return {object} jQuery element\n */\n ControlArea.prototype.getRoot = function() {\n return this.root;\n };\n\n /**\n * Get the container element (which the control area is within).\n *\n * @method getContainer\n * @return {object} jQuery element\n */\n ControlArea.prototype.getContainer = function() {\n return this.container;\n };\n\n /**\n * Get the user id.\n *\n * @method getUserId\n * @return {int}\n */\n ControlArea.prototype.getUserId = function() {\n return this.userId;\n };\n\n /**\n * Get the control area content element.\n *\n * @method getContent\n * @return {object} jQuery element\n */\n ControlArea.prototype.getContent = function() {\n return this.content;\n };\n\n /**\n * Get the offset value for paginated loading of the\n * notifications.\n *\n * @method getOffset\n * @return {int}\n */\n ControlArea.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Get the limit value for the paginated loading of the\n * notifications.\n *\n * @method getLimit\n * @return {int}\n */\n ControlArea.prototype.getLimit = function() {\n return this.limit;\n };\n\n /**\n * Set the offset value for the paginated loading of the\n * notifications.\n *\n * @method setOffset\n * @param {int} value The new offset value\n */\n ControlArea.prototype.setOffset = function(value) {\n this.offset = value;\n };\n\n /**\n * Set the limit value for the paginated loading of the\n * notifications.\n *\n * @method setLimit\n * @param {int} value The new limit value\n */\n ControlArea.prototype.setLimit = function(value) {\n this.limit = value;\n };\n\n /**\n * Increment the offset by the limit amount.\n *\n * @method incrementOffset\n */\n ControlArea.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Flag the control area as loading.\n *\n * @method startLoading\n */\n ControlArea.prototype.startLoading = function() {\n this.isLoading = true;\n this.getRoot().addClass('loading');\n };\n\n /**\n * Remove the loading flag from the control area.\n *\n * @method stopLoading\n */\n ControlArea.prototype.stopLoading = function() {\n this.isLoading = false;\n this.getRoot().removeClass('loading');\n };\n\n /**\n * Check if the first load of notifications has been triggered.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n ControlArea.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if all of the notifications have been loaded.\n *\n * @method hasLoadedAllContent\n * @return {bool}\n */\n ControlArea.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n ControlArea.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Save a notification in the cache.\n *\n * @method setCacheNotification\n * @param {object} notification A notification returned by a webservice\n */\n ControlArea.prototype.setCacheNotification = function(notification) {\n this.notifications[notification.id] = notification;\n };\n\n /**\n * Retrieve a notification from the cache.\n *\n * @method getCacheNotification\n * @param {int} id The id for the notification you wish to retrieve\n * @return {object} A notification (as returned by a webservice)\n */\n ControlArea.prototype.getCacheNotification = function(id) {\n return this.notifications[id];\n };\n\n /**\n * Find the notification element in the control area for the given id.\n *\n * @method getNotificationElement\n * @param {int} id The notification id\n * @return {(object|null)} jQuery element or null\n */\n ControlArea.prototype.getNotificationElement = function(id) {\n var element = this.getRoot().find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Scroll the notification element into view within the control area, if it\n * isn't already visible.\n *\n * @method scrollNotificationIntoView\n * @param {object} notificationElement The jQuery notification element\n */\n ControlArea.prototype.scrollNotificationIntoView = function(notificationElement) {\n var position = notificationElement.position();\n var container = this.getRoot();\n var relativeTop = position.top - container.scrollTop();\n\n // If the element isn't in the view window.\n if (relativeTop > container.innerHeight()) {\n var height = notificationElement.outerHeight();\n // Offset enough to make sure the notification will be in view.\n height = height * 4;\n var scrollTo = position.top - height;\n container.scrollTop(scrollTo);\n }\n };\n\n /**\n * Show the full notification for the given notification element. The notification\n * context is retrieved from the cache and send as data with an event to be\n * rendered in the content area.\n *\n * @method showNotification\n * @param {(int|object)} notificationElement The notification id or jQuery notification element\n */\n ControlArea.prototype.showNotification = function(notificationElement) {\n if (typeof notificationElement !== 'object') {\n // Assume it's an ID if it's not an object.\n notificationElement = this.getNotificationElement(notificationElement);\n }\n\n if (notificationElement && notificationElement.length) {\n this.getRoot().find(SELECTORS.NOTIFICATION).removeClass('selected');\n notificationElement.addClass('selected').find(SELECTORS.CAN_RECEIVE_FOCUS).focus();\n var notificationId = notificationElement.attr('data-id');\n var notification = this.getCacheNotification(notificationId);\n this.scrollNotificationIntoView(notificationElement);\n // Create a new version of the notification to send with the notification so\n // this copy isn't modified.\n this.getContainer().trigger(NotificationAreaEvents.showNotification, [$.extend({}, notification)]);\n }\n };\n\n /**\n * Send a request to mark the notification as read in the server and remove the unread\n * status from the element.\n *\n * @method markNotificationAsRead\n * @param {object} notificationElement The jQuery notification element\n * @return {object} jQuery promise\n */\n ControlArea.prototype.markNotificationAsRead = function(notificationElement) {\n return NotificationRepo.markAsRead(notificationElement.attr('data-id')).done(function() {\n notificationElement.removeClass('unread');\n });\n };\n\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Array of notification data\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n ControlArea.prototype.renderNotifications = function(notifications) {\n var promises = [];\n var container = this.getContent();\n\n $.each(notifications, function(index, notification) {\n // Need to remove the contexturl so the item isn't rendered\n // as a link.\n var contextUrl = notification.contexturl;\n delete notification.contexturl;\n\n var promise = Templates.render(TEMPLATES.NOTIFICATION, notification)\n .then(function(html, js) {\n // Restore it for the cache.\n notification.contexturl = contextUrl;\n this.setCacheNotification(notification);\n // Pass the Rendered content out.\n return {html: html, js: js};\n }.bind(this));\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Load notifications from the server and render them.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise\n */\n ControlArea.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.getLimit(),\n offset: this.getOffset(),\n useridto: this.getUserId(),\n };\n\n if (!this.initialLoad) {\n // If this is the first load we may have been given a non-zero offset,\n // in which case we need to load all notifications preceeding that offset\n // to make sure the full list is rendered.\n request.limit = this.getOffset() + this.getLimit();\n request.offset = 0;\n }\n\n var promise = NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.setLoadedAllContent(!notifications.length || notifications.length < this.getLimit());\n this.initialLoad = true;\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n\n return promise;\n };\n\n /**\n * Create the event listeners for the control area.\n *\n * @method registerEventListeners\n */\n ControlArea.prototype.registerEventListeners = function() {\n CustomEvents.define(this.getRoot(), [\n CustomEvents.events.activate,\n CustomEvents.events.scrollBottom,\n CustomEvents.events.scrollLock,\n CustomEvents.events.up,\n CustomEvents.events.down,\n ]);\n\n this.getRoot().on(CustomEvents.events.scrollBottom, function() {\n this.loadMoreNotifications();\n }.bind(this));\n\n this.getRoot().on(CustomEvents.events.activate, SELECTORS.NOTIFICATION, function(e) {\n var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);\n this.showNotification(notificationElement);\n }.bind(this));\n\n // Show the previous notification in the list.\n this.getRoot().on(CustomEvents.events.up, SELECTORS.NOTIFICATION, function(e, data) {\n var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);\n this.showNotification(notificationElement.prev());\n\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Show the next notification in the list.\n this.getRoot().on(CustomEvents.events.down, SELECTORS.NOTIFICATION, function(e, data) {\n var notificationElement = $(e.target).closest(SELECTORS.NOTIFICATION);\n this.showNotification(notificationElement.next());\n\n data.originalEvent.preventDefault();\n }.bind(this));\n\n this.getContainer().on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n this.markNotificationAsRead(element);\n }\n\n var cachedNotification = this.getCacheNotification(notification.id);\n\n if (cachedNotification) {\n cachedNotification.read = true;\n }\n }\n }.bind(this));\n };\n\n return ControlArea;\n});\n"],"names":["define","$","Templates","DebugNotification","CustomEvents","NotificationRepo","NotificationAreaEvents","SELECTORS","TEMPLATES","ControlArea","root","userId","container","this","closest","content","find","offset","limit","initialLoad","isLoading","loadedAll","notifications","registerEventListeners","prototype","getRoot","getContainer","getUserId","getContent","getOffset","getLimit","setOffset","value","setLimit","incrementOffset","startLoading","addClass","stopLoading","removeClass","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","setCacheNotification","notification","id","getCacheNotification","getNotificationElement","element","length","scrollNotificationIntoView","notificationElement","position","top","scrollTop","innerHeight","height","outerHeight","scrollTo","showNotification","focus","notificationId","attr","trigger","extend","markNotificationAsRead","markAsRead","done","renderNotifications","promises","each","index","contextUrl","contexturl","promise","render","then","html","js","bind","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","Deferred","resolve","request","useridto","query","result","unreadCount","unreadcount","always","events","activate","scrollBottom","scrollLock","up","down","on","e","target","data","prev","originalEvent","preventDefault","next","notificationShown","read","cachedNotification"],"mappings":";;;;;;;AAsBAA,sDAAO,CAAC,SAAU,iBAAkB,oBAAqB,iCACjD,wCAAyC,2CAC7C,SAASC,EAAGC,UAAWC,kBAAmBC,aAAcC,iBAAkBC,4BAEtEC,oBACW,oCADXA,kBAES,0BAFTA,uBAGc,sDAHdA,4BAImB,4EAGnBC,uBACc,0CAUdC,YAAc,SAASC,KAAMC,aACxBD,KAAOT,EAAES,WACTE,UAAYC,KAAKH,KAAKI,QAAQP,0BAC9BI,OAASA,YACTI,QAAUF,KAAKH,KAAKM,KAAKT,wBACzBU,OAAS,OACTC,MAAQ,QACRC,aAAc,OACdC,WAAY,OACZC,WAAY,OACZC,cAAgB,QAEhBC,iCASTd,YAAYe,UAAUC,QAAU,kBACrBZ,KAAKH,MAShBD,YAAYe,UAAUE,aAAe,kBAC1Bb,KAAKD,WAShBH,YAAYe,UAAUG,UAAY,kBACvBd,KAAKF,QAShBF,YAAYe,UAAUI,WAAa,kBACxBf,KAAKE,SAUhBN,YAAYe,UAAUK,UAAY,kBACvBhB,KAAKI,QAUhBR,YAAYe,UAAUM,SAAW,kBACtBjB,KAAKK,OAUhBT,YAAYe,UAAUO,UAAY,SAASC,YAClCf,OAASe,OAUlBvB,YAAYe,UAAUS,SAAW,SAASD,YACjCd,MAAQc,OAQjBvB,YAAYe,UAAUU,gBAAkB,gBAC/BjB,QAAUJ,KAAKK,OAQxBT,YAAYe,UAAUW,aAAe,gBAC5Bf,WAAY,OACZK,UAAUW,SAAS,YAQ5B3B,YAAYe,UAAUa,YAAc,gBAC3BjB,WAAY,OACZK,UAAUa,YAAY,YAS/B7B,YAAYe,UAAUe,mBAAqB,kBAChC1B,KAAKM,aAShBV,YAAYe,UAAUgB,oBAAsB,kBACjC3B,KAAKQ,WAShBZ,YAAYe,UAAUiB,oBAAsB,SAASC,UAC5CrB,UAAYqB,KASrBjC,YAAYe,UAAUmB,qBAAuB,SAASC,mBAC7CtB,cAAcsB,aAAaC,IAAMD,cAU1CnC,YAAYe,UAAUsB,qBAAuB,SAASD,WAC3ChC,KAAKS,cAAcuB,KAU9BpC,YAAYe,UAAUuB,uBAAyB,SAASF,QAChDG,QAAUnC,KAAKY,UAAUT,KAAKT,uBAAyB,aAAesC,GAAK,aACtD,GAAlBG,QAAQC,OAAcD,QAAU,MAU3CvC,YAAYe,UAAU0B,2BAA6B,SAASC,yBACpDC,SAAWD,oBAAoBC,WAC/BxC,UAAYC,KAAKY,aACH2B,SAASC,IAAMzC,UAAU0C,YAGzB1C,UAAU2C,cAAe,KACnCC,OAASL,oBAAoBM,cAEjCD,QAAkB,MACdE,SAAWN,SAASC,IAAMG,OAC9B5C,UAAU0C,UAAUI,YAY5BjD,YAAYe,UAAUmC,iBAAmB,SAASR,wBACX,iBAAxBA,sBAEPA,oBAAsBtC,KAAKkC,uBAAuBI,sBAGlDA,qBAAuBA,oBAAoBF,OAAQ,MAC9CxB,UAAUT,KAAKT,wBAAwB+B,YAAY,YACxDa,oBAAoBf,SAAS,YAAYpB,KAAKT,6BAA6BqD,YACvEC,eAAiBV,oBAAoBW,KAAK,WAC1ClB,aAAe/B,KAAKiC,qBAAqBe,qBACxCX,2BAA2BC,0BAG3BzB,eAAeqC,QAAQzD,uBAAuBqD,iBAAkB,CAAC1D,EAAE+D,OAAO,GAAIpB,kBAY3FnC,YAAYe,UAAUyC,uBAAyB,SAASd,4BAC7C9C,iBAAiB6D,WAAWf,oBAAoBW,KAAK,YAAYK,MAAK,WACzEhB,oBAAoBb,YAAY,cAaxC7B,YAAYe,UAAU4C,oBAAsB,SAAS9C,mBAC7C+C,SAAW,GACXzD,UAAYC,KAAKe,oBAErB3B,EAAEqE,KAAKhD,cAAe,SAASiD,MAAO3B,kBAG9B4B,WAAa5B,aAAa6B,kBACvB7B,aAAa6B,eAEhBC,QAAUxE,UAAUyE,OAAOnE,uBAAwBoC,cACtDgC,KAAK,SAASC,KAAMC,WAEjBlC,aAAa6B,WAAaD,gBACrB7B,qBAAqBC,cAEnB,CAACiC,KAAMA,KAAMC,GAAIA,KAC1BC,KAAKlE,OACPwD,SAASW,KAAKN,UAChBK,KAAKlE,OAEAZ,EAAEgF,KAAKC,MAAMjF,EAAGoE,UAAUO,MAAK,WAIlC3E,EAAEqE,KAAKa,WAAW,SAASZ,MAAOa,UAC9BxE,UAAUyE,OAAOD,SAASP,MAC1B3E,UAAUoF,cAAcF,SAASN,WAY7CrE,YAAYe,UAAU+D,sBAAwB,cACtC1E,KAAKO,WAAaP,KAAK2B,6BAChBvC,EAAEuF,WAAWC,eAGnBtD,mBACDuD,QAAU,CACVxE,MAAOL,KAAKiB,WACZb,OAAQJ,KAAKgB,YACb8D,SAAU9E,KAAKc,oBAGdd,KAAKM,cAINuE,QAAQxE,MAAQL,KAAKgB,YAAchB,KAAKiB,WACxC4D,QAAQzE,OAAS,GAGPZ,iBAAiBuF,MAAMF,SAASd,KAAK,SAASiB,YACpDvE,cAAgBuE,OAAOvE,0BACtBwE,YAAcD,OAAOE,iBACrBtD,qBAAqBnB,cAAc2B,QAAU3B,cAAc2B,OAASpC,KAAKiB,iBACzEX,aAAc,IAEfG,cAAc2B,cACTf,kBACErB,KAAKuD,oBAAoB9C,iBAItCyD,KAAKlE,OACNmF,OAAO,gBACC3D,eACP0C,KAAKlE,QAUXJ,YAAYe,UAAUD,uBAAyB,WAC3CnB,aAAaJ,OAAOa,KAAKY,UAAW,CAChCrB,aAAa6F,OAAOC,SACpB9F,aAAa6F,OAAOE,aACpB/F,aAAa6F,OAAOG,WACpBhG,aAAa6F,OAAOI,GACpBjG,aAAa6F,OAAOK,YAGnB7E,UAAU8E,GAAGnG,aAAa6F,OAAOE,aAAc,gBAC3CZ,yBACPR,KAAKlE,YAEFY,UAAU8E,GAAGnG,aAAa6F,OAAOC,SAAU3F,uBAAwB,SAASiG,OACzErD,oBAAsBlD,EAAEuG,EAAEC,QAAQ3F,QAAQP,6BACzCoD,iBAAiBR,sBACxB4B,KAAKlE,YAGFY,UAAU8E,GAAGnG,aAAa6F,OAAOI,GAAI9F,uBAAwB,SAASiG,EAAGE,UACtEvD,oBAAsBlD,EAAEuG,EAAEC,QAAQ3F,QAAQP,6BACzCoD,iBAAiBR,oBAAoBwD,QAE1CD,KAAKE,cAAcC,kBACrB9B,KAAKlE,YAGFY,UAAU8E,GAAGnG,aAAa6F,OAAOK,KAAM/F,uBAAwB,SAASiG,EAAGE,UACxEvD,oBAAsBlD,EAAEuG,EAAEC,QAAQ3F,QAAQP,6BACzCoD,iBAAiBR,oBAAoB2D,QAE1CJ,KAAKE,cAAcC,kBACrB9B,KAAKlE,YAEFa,eAAe6E,GAAGjG,uBAAuByG,kBAAmB,SAASP,EAAG5D,kBACpEA,aAAaoE,KAAM,KAChBhE,QAAUnC,KAAKkC,uBAAuBH,aAAaC,IAEnDG,cACKiB,uBAAuBjB,aAG5BiE,mBAAqBpG,KAAKiC,qBAAqBF,aAAaC,IAE5DoE,qBACAA,mBAAmBD,MAAO,KAGpCjC,KAAKlE,QAGJJ"}PKS\Y@]UZUZ4amd/build/notification_popover_controller.min.js.mapnu[{"version":3,"file":"notification_popover_controller.min.js","sources":["../src/notification_popover_controller.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls the notification popover in the nav bar.\n *\n * See template: message_popup/notification_popover\n *\n * @module message_popup/notification_popover_controller\n * @class notification_popover_controller\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',\n 'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',\n 'message_popup/notification_repository', 'message_popup/notification_area_events',\n 'core/local/aria/focuslock',\n ],\n function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,\n PopoverController, NotificationRepo, NotificationAreaEvents, FocusLock) {\n\n var SELECTORS = {\n MARK_ALL_READ_BUTTON: '[data-action=\"mark-all-read\"]',\n ALL_NOTIFICATIONS_CONTAINER: '[data-region=\"all-notifications\"]',\n NOTIFICATION: '[data-region=\"notification-content-item-container\"]',\n UNREAD_NOTIFICATION: '[data-region=\"notification-content-item-container\"].unread',\n NOTIFICATION_LINK: '[data-action=\"content-item-link\"]',\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n COUNT_CONTAINER: '[data-region=\"count-container\"]',\n NOTIFICATION_READ_FEEDBACK: '[data-region=\"notification-read-feedback\"]',\n CLOSE_NOTIFICATION_POPOVER: '[data-action=\"close-notification-popover\"]',\n };\n\n /**\n * Constructor for the NotificationPopoverController.\n * Extends PopoverRegionController.\n *\n * @param {object} element jQuery object root element of the popover\n */\n var NotificationPopoverController = function(element) {\n // Initialise base class.\n PopoverController.call(this, element);\n\n this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);\n this.unreadCount = 0;\n this.lastQueried = 0;\n this.userId = this.root.attr('data-userid');\n this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);\n this.limit = 20;\n this.offset = 0;\n this.loadedAll = false;\n this.initialLoad = false;\n\n // Let's find out how many unread notifications there are.\n this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();\n };\n\n /**\n * Clone the parent prototype.\n */\n NotificationPopoverController.prototype = Object.create(PopoverController.prototype);\n\n /**\n * Make sure the constructor is set correctly.\n */\n NotificationPopoverController.prototype.constructor = NotificationPopoverController;\n\n /**\n * Set the correct aria label on the menu toggle button to be read out by screen\n * readers. The message will indicate the state of the unread notifications.\n *\n * @method updateButtonAriaLabel\n */\n NotificationPopoverController.prototype.updateButtonAriaLabel = function() {\n if (this.isMenuOpen()) {\n Str.get_string('hidenotificationwindow', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n if (this.unreadCount) {\n Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n } else {\n Str.get_string('shownotificationwindownonew', 'message').done(function(string) {\n this.menuToggle.attr('aria-label', string);\n }.bind(this));\n }\n }\n };\n\n /**\n * Return the jQuery element with the content. This will return either\n * the unread notification container or the all notification container\n * depending on which is currently visible.\n *\n * @method getContent\n * @return {object} jQuery object currently visible content contianer\n */\n NotificationPopoverController.prototype.getContent = function() {\n return this.container;\n };\n\n /**\n * Get the offset value for the current state of the popover in order\n * to sent to the backend to correctly paginate the notifications.\n *\n * @method getOffset\n * @return {int} current offset\n */\n NotificationPopoverController.prototype.getOffset = function() {\n return this.offset;\n };\n\n /**\n * Increment the offset for the current state, if required.\n *\n * @method incrementOffset\n */\n NotificationPopoverController.prototype.incrementOffset = function() {\n this.offset += this.limit;\n };\n\n /**\n * Check if the first load of notification has been triggered for the current\n * state of the popover.\n *\n * @method hasDoneInitialLoad\n * @return {bool} true if first notification loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasDoneInitialLoad = function() {\n return this.initialLoad;\n };\n\n /**\n * Check if we've loaded all of the notifications for the current popover\n * state.\n *\n * @method hasLoadedAllContent\n * @return {bool} true if all notifications loaded, false otherwise\n */\n NotificationPopoverController.prototype.hasLoadedAllContent = function() {\n return this.loadedAll;\n };\n\n /**\n * Set the state of the loaded all content property for the current state\n * of the popover.\n *\n * @method setLoadedAllContent\n * @param {bool} val True if all content is loaded, false otherwise\n */\n NotificationPopoverController.prototype.setLoadedAllContent = function(val) {\n this.loadedAll = val;\n };\n\n /**\n * Show the unread notification count badge on the menu toggle if there\n * are unread notifications, otherwise hide it.\n *\n * @method renderUnreadCount\n */\n NotificationPopoverController.prototype.renderUnreadCount = function() {\n var element = this.root.find(SELECTORS.COUNT_CONTAINER);\n\n if (this.unreadCount) {\n element.text(this.unreadCount);\n element.removeClass('hidden');\n } else {\n element.addClass('hidden');\n }\n };\n\n /**\n * Hide the unread notification count badge on the menu toggle.\n *\n * @method hideUnreadCount\n */\n NotificationPopoverController.prototype.hideUnreadCount = function() {\n this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');\n };\n\n /**\n * Find the notification element for the given id.\n *\n * @param {int} id\n * @method getNotificationElement\n * @return {object|null} The notification element\n */\n NotificationPopoverController.prototype.getNotificationElement = function(id) {\n var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id=\"' + id + '\"]');\n return element.length == 1 ? element : null;\n };\n\n /**\n * Render the notification data with the appropriate template and add it to the DOM.\n *\n * @method renderNotifications\n * @param {array} notifications Notification data\n * @param {object} container jQuery object the container to append the rendered notifications\n * @return {object} jQuery promise that is resolved when all notifications have been\n * rendered and added to the DOM\n */\n NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {\n var promises = [];\n\n $.each(notifications, function(index, notification) {\n // Determine what the offset was when loading this notification.\n var offset = this.getOffset() - this.limit;\n // Update the view more url to contain the offset to allow the notifications\n // page to load to the correct position in the list of notifications.\n notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {\n notificationid: notification.id,\n offset: offset,\n });\n\n // Link to mark read page before loading the actual link.\n var notificationurlparams = {\n notificationid: notification.id\n };\n\n notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);\n\n var promise = Templates.render('message_popup/notification_content_item', notification)\n .then(function(html, js) {\n return {html: html, js: js};\n });\n promises.push(promise);\n }.bind(this));\n\n return $.when.apply($, promises).then(function() {\n // Each of the promises in the when will pass its results as an argument to the function.\n // The order of the arguments will be the order that the promises are passed to when()\n // i.e. the first promise's results will be in the first argument.\n $.each(arguments, function(index, argument) {\n container.append(argument.html);\n Templates.runTemplateJS(argument.js);\n });\n return;\n });\n };\n\n /**\n * Send a request for more notifications from the server, if we aren't already\n * loading some and haven't already loaded all of them.\n *\n * Takes into account the current mode of the popover and will request only\n * unread notifications if required.\n *\n * All notifications are marked as read by the server when they are returned.\n *\n * @method loadMoreNotifications\n * @return {object} jQuery promise that is resolved when notifications have been\n * retrieved and added to the DOM\n */\n NotificationPopoverController.prototype.loadMoreNotifications = function() {\n if (this.isLoading || this.hasLoadedAllContent()) {\n return $.Deferred().resolve();\n }\n\n this.startLoading();\n var request = {\n limit: this.limit,\n offset: this.getOffset(),\n useridto: this.userId,\n };\n\n var container = this.getContent();\n return NotificationRepo.query(request).then(function(result) {\n var notifications = result.notifications;\n this.unreadCount = result.unreadcount;\n this.lastQueried = Math.floor(new Date().getTime() / 1000);\n this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);\n this.initialLoad = true;\n this.updateButtonAriaLabel();\n\n if (notifications.length) {\n this.incrementOffset();\n return this.renderNotifications(notifications, container);\n }\n\n return false;\n }.bind(this))\n .always(function() {\n this.stopLoading();\n }.bind(this));\n };\n\n /**\n * Send a request to the server to mark all unread notifications as read and update\n * the unread count and unread notification elements appropriately.\n *\n * @return {Promise}\n * @method markAllAsRead\n */\n NotificationPopoverController.prototype.markAllAsRead = function() {\n this.markAllReadButton.addClass('loading');\n\n var request = {\n useridto: this.userId,\n timecreatedto: this.lastQueried,\n };\n\n return NotificationRepo.markAllAsRead(request)\n .then(function() {\n this.unreadCount = 0;\n this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');\n\n // Set the ARIA live region's contents with the feedback.\n const readFeedback = this.root.get(0).querySelector(SELECTORS.NOTIFICATION_READ_FEEDBACK);\n Str.get_string('notificationsmarkedasread', 'message').done((notificationsmarkedasread) => {\n readFeedback.innerHTML = notificationsmarkedasread;\n });\n }.bind(this))\n .always(function() {\n this.markAllReadButton.removeClass('loading');\n }.bind(this));\n };\n\n /**\n * Add all of the required event listeners for this notification popover.\n *\n * @method registerEventListeners\n */\n NotificationPopoverController.prototype.registerEventListeners = function() {\n CustomEvents.define(this.root, [\n CustomEvents.events.activate,\n ]);\n\n // Mark all notifications read if the user activates the mark all as read button.\n this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {\n if (this.unreadCount > 0) {\n this.markAllAsRead();\n }\n\n e.stopPropagation();\n data.originalEvent.preventDefault();\n }.bind(this));\n\n // Mark individual notification read if the user activates it.\n this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {\n var element = $(e.target).closest(SELECTORS.NOTIFICATION);\n\n if (element.hasClass('unread')) {\n this.unreadCount--;\n element.removeClass('unread');\n }\n\n e.stopPropagation();\n }.bind(this));\n\n this.root.on(CustomEvents.events.activate, SELECTORS.CLOSE_NOTIFICATION_POPOVER, function(e) {\n e.preventDefault();\n $(this.root).trigger(CustomEvents.events.escape);\n e.stopPropagation();\n }.bind(this));\n\n // Update the notification information when the menu is opened.\n this.root.on(this.events().menuOpened, function() {\n this.hideUnreadCount();\n this.updateButtonAriaLabel();\n\n if (!this.hasDoneInitialLoad()) {\n this.loadMoreNotifications();\n }\n\n // Lock focus to the popover when it is opened, it is the parent of the container.\n const contentContainer = this.getContentContainer()[0].parentNode;\n FocusLock.trapFocus(contentContainer);\n\n }.bind(this));\n\n // Update the unread notification count when the menu is closed.\n this.root.on(this.events().menuClosed, function() {\n this.renderUnreadCount();\n this.updateButtonAriaLabel();\n // Lock focus to the popover when it is opened, it is the parent of the container.\n FocusLock.untrapFocus();\n }.bind(this));\n\n // Set aria attributes when popover is loading.\n this.root.on(this.events().startLoading, function() {\n this.getContent().attr('aria-busy', 'true');\n }.bind(this));\n\n // Set aria attributes when popover is finished loading.\n this.root.on(this.events().stopLoading, function() {\n this.getContent().attr('aria-busy', 'false');\n }.bind(this));\n\n // Load more notifications if the user has scrolled to the end of content\n // item list.\n this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {\n if (!this.isLoading && !this.hasLoadedAllContent()) {\n this.loadMoreNotifications();\n }\n }.bind(this));\n\n // Stop mouse scroll from propagating to the window element and\n // scrolling the page.\n CustomEvents.define(this.getContentContainer(), [\n CustomEvents.events.scrollLock\n ]);\n\n // Listen for when a notification is shown in the notifications page and mark\n // it as read, if it's unread.\n $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {\n if (!notification.read) {\n var element = this.getNotificationElement(notification.id);\n\n if (element) {\n element.removeClass('unread');\n }\n\n this.unreadCount--;\n this.renderUnreadCount();\n }\n }.bind(this));\n };\n\n return NotificationPopoverController;\n});\n"],"names":["define","$","Ajax","Templates","Str","URL","DebugNotification","CustomEvents","PopoverController","NotificationRepo","NotificationAreaEvents","FocusLock","SELECTORS","NotificationPopoverController","element","call","this","markAllReadButton","root","find","unreadCount","lastQueried","userId","attr","container","limit","offset","loadedAll","initialLoad","html","prototype","Object","create","constructor","updateButtonAriaLabel","isMenuOpen","get_string","done","string","menuToggle","bind","getContent","getOffset","incrementOffset","hasDoneInitialLoad","hasLoadedAllContent","setLoadedAllContent","val","renderUnreadCount","text","removeClass","addClass","hideUnreadCount","getNotificationElement","id","length","renderNotifications","notifications","promises","each","index","notification","viewmoreurl","relativeUrl","notificationid","notificationurlparams","contexturl","promise","render","then","js","push","when","apply","arguments","argument","append","runTemplateJS","loadMoreNotifications","isLoading","Deferred","resolve","startLoading","request","useridto","query","result","unreadcount","Math","floor","Date","getTime","always","stopLoading","markAllAsRead","timecreatedto","readFeedback","get","querySelector","notificationsmarkedasread","innerHTML","registerEventListeners","events","activate","on","e","data","stopPropagation","originalEvent","preventDefault","target","closest","hasClass","trigger","escape","menuOpened","contentContainer","getContentContainer","parentNode","trapFocus","menuClosed","untrapFocus","scrollBottom","scrollLock","document","notificationShown","read"],"mappings":";;;;;;;;;;AAyBAA,uDAAO,CAAC,SAAU,YAAa,iBAAkB,WAAY,WACjD,oBAAqB,iCAAkC,iCACvD,wCAAyC,yCACzC,8BAEJ,SAASC,EAAGC,KAAMC,UAAWC,IAAKC,IAAKC,kBAAmBC,aACtDC,kBAAmBC,iBAAkBC,uBAAwBC,eAEjEC,+BACsB,gCADtBA,sCAE6B,oCAF7BA,uBAGc,sDAHdA,8BAIqB,6DAJrBA,4BAKmB,oCALnBA,0BAOiB,kCAPjBA,qCAQ4B,6CAR5BA,qCAS4B,6CAS5BC,8BAAgC,SAASC,SAEzCN,kBAAkBO,KAAKC,KAAMF,cAExBG,kBAAoBD,KAAKE,KAAKC,KAAKP,qCACnCQ,YAAc,OACdC,YAAc,OACdC,OAASN,KAAKE,KAAKK,KAAK,oBACxBC,UAAYR,KAAKE,KAAKC,KAAKP,4CAC3Ba,MAAQ,QACRC,OAAS,OACTC,WAAY,OACZC,aAAc,OAGdR,YAAcJ,KAAKE,KAAKC,KAAKP,2BAA2BiB,eAMjEhB,8BAA8BiB,UAAYC,OAAOC,OAAOxB,kBAAkBsB,YAKlCG,YAAcpB,8BAQtDA,8BAA8BiB,UAAUI,sBAAwB,WACxDlB,KAAKmB,aACL/B,IAAIgC,WAAW,yBAA0B,WAAWC,KAAK,SAASC,aACzDC,WAAWhB,KAAK,aAAce,SACrCE,KAAKxB,OAEHA,KAAKI,YACLhB,IAAIgC,WAAW,kCAAmC,UAAWpB,KAAKI,aAAaiB,KAAK,SAASC,aACpFC,WAAWhB,KAAK,aAAce,SACrCE,KAAKxB,OAEPZ,IAAIgC,WAAW,8BAA+B,WAAWC,KAAK,SAASC,aAC9DC,WAAWhB,KAAK,aAAce,SACrCE,KAAKxB,QAanBH,8BAA8BiB,UAAUW,WAAa,kBAC1CzB,KAAKQ,WAUhBX,8BAA8BiB,UAAUY,UAAY,kBACzC1B,KAAKU,QAQhBb,8BAA8BiB,UAAUa,gBAAkB,gBACjDjB,QAAUV,KAAKS,OAUxBZ,8BAA8BiB,UAAUc,mBAAqB,kBAClD5B,KAAKY,aAUhBf,8BAA8BiB,UAAUe,oBAAsB,kBACnD7B,KAAKW,WAUhBd,8BAA8BiB,UAAUgB,oBAAsB,SAASC,UAC9DpB,UAAYoB,KASrBlC,8BAA8BiB,UAAUkB,kBAAoB,eACpDlC,QAAUE,KAAKE,KAAKC,KAAKP,2BAEzBI,KAAKI,aACLN,QAAQmC,KAAKjC,KAAKI,aAClBN,QAAQoC,YAAY,WAEpBpC,QAAQqC,SAAS,WASzBtC,8BAA8BiB,UAAUsB,gBAAkB,gBACjDlC,KAAKC,KAAKP,2BAA2BuC,SAAS,WAUvDtC,8BAA8BiB,UAAUuB,uBAAyB,SAASC,QAClExC,QAAUE,KAAKE,KAAKC,KAAKP,uBAAyB,aAAe0C,GAAK,aACjD,GAAlBxC,QAAQyC,OAAczC,QAAU,MAY3CD,8BAA8BiB,UAAU0B,oBAAsB,SAASC,cAAejC,eAC9EkC,SAAW,UAEfzD,EAAE0D,KAAKF,cAAe,SAASG,MAAOC,kBAE9BnC,OAASV,KAAK0B,YAAc1B,KAAKS,MAGrCoC,aAAaC,YAAczD,IAAI0D,YAAY,0CAA2C,CAClFC,eAAgBH,aAAaP,GAC7B5B,OAAQA,aAIRuC,sBAAwB,CACxBD,eAAgBH,aAAaP,IAGjCO,aAAaK,WAAa7D,IAAI0D,YAAY,kDAAmDE,2BAEzFE,QAAUhE,UAAUiE,OAAO,0CAA2CP,cACzEQ,MAAK,SAASxC,KAAMyC,UACV,CAACzC,KAAMA,KAAMyC,GAAIA,OAE5BZ,SAASa,KAAKJ,UAChB3B,KAAKxB,OAEAf,EAAEuE,KAAKC,MAAMxE,EAAGyD,UAAUW,MAAK,WAIlCpE,EAAE0D,KAAKe,WAAW,SAASd,MAAOe,UAC9BnD,UAAUoD,OAAOD,SAAS9C,MAC1B1B,UAAU0E,cAAcF,SAASL,WAmB7CzD,8BAA8BiB,UAAUgD,sBAAwB,cACxD9D,KAAK+D,WAAa/D,KAAK6B,6BAChB5C,EAAE+E,WAAWC,eAGnBC,mBACDC,QAAU,CACV1D,MAAOT,KAAKS,MACZC,OAAQV,KAAK0B,YACb0C,SAAUpE,KAAKM,QAGfE,UAAYR,KAAKyB,oBACdhC,iBAAiB4E,MAAMF,SAASd,KAAK,SAASiB,YAC7C7B,cAAgB6B,OAAO7B,0BACtBrC,YAAckE,OAAOC,iBACrBlE,YAAcmE,KAAKC,OAAM,IAAIC,MAAOC,UAAY,UAChD7C,qBAAqBW,cAAcF,QAAUE,cAAcF,OAASvC,KAAKS,YACzEG,aAAc,OACdM,0BAEDuB,cAAcF,cACTZ,kBACE3B,KAAKwC,oBAAoBC,cAAejC,aAIrDgB,KAAKxB,OACN4E,OAAO,gBACCC,eACPrD,KAAKxB,QAUXH,8BAA8BiB,UAAUgE,cAAgB,gBAC/C7E,kBAAkBkC,SAAS,eAE5BgC,QAAU,CACVC,SAAUpE,KAAKM,OACfyE,cAAe/E,KAAKK,oBAGjBZ,iBAAiBqF,cAAcX,SACjCd,KAAK,gBACGjD,YAAc,OACdF,KAAKC,KAAKP,+BAA+BsC,YAAY,gBAGpD8C,aAAehF,KAAKE,KAAK+E,IAAI,GAAGC,cAActF,sCACpDR,IAAIgC,WAAW,4BAA6B,WAAWC,MAAM8D,4BACzDH,aAAaI,UAAYD,8BAE/B3D,KAAKxB,OACN4E,OAAO,gBACC3E,kBAAkBiC,YAAY,YACrCV,KAAKxB,QAQfH,8BAA8BiB,UAAUuE,uBAAyB,WAC7D9F,aAAaP,OAAOgB,KAAKE,KAAM,CAC3BX,aAAa+F,OAAOC,gBAInBrF,KAAKsF,GAAGjG,aAAa+F,OAAOC,SAAU3F,+BAAgC,SAAS6F,EAAGC,MAC/E1F,KAAKI,YAAc,QACd0E,gBAGTW,EAAEE,kBACFD,KAAKE,cAAcC,kBACrBrE,KAAKxB,YAGFE,KAAKsF,GAAGjG,aAAa+F,OAAOC,SAAU3F,4BAA6B,SAAS6F,OACzE3F,QAAUb,EAAEwG,EAAEK,QAAQC,QAAQnG,wBAE9BE,QAAQkG,SAAS,iBACZ5F,cACLN,QAAQoC,YAAY,WAGxBuD,EAAEE,mBACJnE,KAAKxB,YAEFE,KAAKsF,GAAGjG,aAAa+F,OAAOC,SAAU3F,qCAAsC,SAAS6F,GACtFA,EAAEI,iBACF5G,EAAEe,KAAKE,MAAM+F,QAAQ1G,aAAa+F,OAAOY,QACzCT,EAAEE,mBACJnE,KAAKxB,YAGFE,KAAKsF,GAAGxF,KAAKsF,SAASa,WAAY,gBAC9B/D,uBACAlB,wBAEAlB,KAAK4B,2BACDkC,8BAIHsC,iBAAmBpG,KAAKqG,sBAAsB,GAAGC,WACvD3G,UAAU4G,UAAUH,mBAEtB5E,KAAKxB,YAGFE,KAAKsF,GAAGxF,KAAKsF,SAASkB,WAAY,gBAC9BxE,yBACAd,wBAELvB,UAAU8G,eACZjF,KAAKxB,YAGFE,KAAKsF,GAAGxF,KAAKsF,SAASpB,aAAc,gBAChCzC,aAAalB,KAAK,YAAa,SACtCiB,KAAKxB,YAGFE,KAAKsF,GAAGxF,KAAKsF,SAAST,YAAa,gBAC/BpD,aAAalB,KAAK,YAAa,UACtCiB,KAAKxB,YAIFqG,sBAAsBb,GAAGjG,aAAa+F,OAAOoB,aAAc,WACvD1G,KAAK+D,WAAc/D,KAAK6B,4BACpBiC,yBAEXtC,KAAKxB,OAIPT,aAAaP,OAAOgB,KAAKqG,sBAAuB,CAC5C9G,aAAa+F,OAAOqB,aAKxB1H,EAAE2H,UAAUpB,GAAG9F,uBAAuBmH,kBAAmB,SAASpB,EAAG5C,kBAC5DA,aAAaiE,KAAM,KAChBhH,QAAUE,KAAKqC,uBAAuBQ,aAAaP,IAEnDxC,SACAA,QAAQoC,YAAY,eAGnB9B,mBACA4B,sBAEXR,KAAKxB,QAGJH"}PKS\q/amd/build/notification_area_control_area.min.jsnu[/** * Controls the notification area on the notification page. * * @module message_popup/notification_area_control_area * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("message_popup/notification_area_control_area",["jquery","core/templates","core/notification","core/custom_interaction_events","message_popup/notification_repository","message_popup/notification_area_events"],(function($,Templates,DebugNotification,CustomEvents,NotificationRepo,NotificationAreaEvents){var SELECTORS_CONTAINER='[data-region="notification-area"]',SELECTORS_CONTENT='[data-region="content"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_CAN_RECEIVE_FOCUS='input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',TEMPLATES_NOTIFICATION="message_popup/notification_content_item",ControlArea=function(root,userId){this.root=$(root),this.container=this.root.closest(SELECTORS_CONTAINER),this.userId=userId,this.content=this.root.find(SELECTORS_CONTENT),this.offset=0,this.limit=20,this.initialLoad=!1,this.isLoading=!1,this.loadedAll=!1,this.notifications={},this.registerEventListeners()};return ControlArea.prototype.getRoot=function(){return this.root},ControlArea.prototype.getContainer=function(){return this.container},ControlArea.prototype.getUserId=function(){return this.userId},ControlArea.prototype.getContent=function(){return this.content},ControlArea.prototype.getOffset=function(){return this.offset},ControlArea.prototype.getLimit=function(){return this.limit},ControlArea.prototype.setOffset=function(value){this.offset=value},ControlArea.prototype.setLimit=function(value){this.limit=value},ControlArea.prototype.incrementOffset=function(){this.offset+=this.limit},ControlArea.prototype.startLoading=function(){this.isLoading=!0,this.getRoot().addClass("loading")},ControlArea.prototype.stopLoading=function(){this.isLoading=!1,this.getRoot().removeClass("loading")},ControlArea.prototype.hasDoneInitialLoad=function(){return this.initialLoad},ControlArea.prototype.hasLoadedAllContent=function(){return this.loadedAll},ControlArea.prototype.setLoadedAllContent=function(val){this.loadedAll=val},ControlArea.prototype.setCacheNotification=function(notification){this.notifications[notification.id]=notification},ControlArea.prototype.getCacheNotification=function(id){return this.notifications[id]},ControlArea.prototype.getNotificationElement=function(id){var element=this.getRoot().find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},ControlArea.prototype.scrollNotificationIntoView=function(notificationElement){var position=notificationElement.position(),container=this.getRoot();if(position.top-container.scrollTop()>container.innerHeight()){var height=notificationElement.outerHeight();height*=4;var scrollTo=position.top-height;container.scrollTop(scrollTo)}},ControlArea.prototype.showNotification=function(notificationElement){if("object"!=typeof notificationElement&&(notificationElement=this.getNotificationElement(notificationElement)),notificationElement&¬ificationElement.length){this.getRoot().find(SELECTORS_NOTIFICATION).removeClass("selected"),notificationElement.addClass("selected").find(SELECTORS_CAN_RECEIVE_FOCUS).focus();var notificationId=notificationElement.attr("data-id"),notification=this.getCacheNotification(notificationId);this.scrollNotificationIntoView(notificationElement),this.getContainer().trigger(NotificationAreaEvents.showNotification,[$.extend({},notification)])}},ControlArea.prototype.markNotificationAsRead=function(notificationElement){return NotificationRepo.markAsRead(notificationElement.attr("data-id")).done((function(){notificationElement.removeClass("unread")}))},ControlArea.prototype.renderNotifications=function(notifications){var promises=[],container=this.getContent();return $.each(notifications,function(index,notification){var contextUrl=notification.contexturl;delete notification.contexturl;var promise=Templates.render(TEMPLATES_NOTIFICATION,notification).then(function(html,js){return notification.contexturl=contextUrl,this.setCacheNotification(notification),{html:html,js:js}}.bind(this));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},ControlArea.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.getLimit(),offset:this.getOffset(),useridto:this.getUserId()};return this.initialLoad||(request.limit=this.getOffset()+this.getLimit(),request.offset=0),NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.setLoadedAllContent(!notifications.length||notifications.length * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("message_popup/notification_popover_controller",["jquery","core/ajax","core/templates","core/str","core/url","core/notification","core/custom_interaction_events","core/popover_region_controller","message_popup/notification_repository","message_popup/notification_area_events","core/local/aria/focuslock"],(function($,Ajax,Templates,Str,URL,DebugNotification,CustomEvents,PopoverController,NotificationRepo,NotificationAreaEvents,FocusLock){var SELECTORS_MARK_ALL_READ_BUTTON='[data-action="mark-all-read"]',SELECTORS_ALL_NOTIFICATIONS_CONTAINER='[data-region="all-notifications"]',SELECTORS_NOTIFICATION='[data-region="notification-content-item-container"]',SELECTORS_UNREAD_NOTIFICATION='[data-region="notification-content-item-container"].unread',SELECTORS_NOTIFICATION_LINK='[data-action="content-item-link"]',SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',SELECTORS_NOTIFICATION_READ_FEEDBACK='[data-region="notification-read-feedback"]',SELECTORS_CLOSE_NOTIFICATION_POPOVER='[data-action="close-notification-popover"]',NotificationPopoverController=function(element){PopoverController.call(this,element),this.markAllReadButton=this.root.find(SELECTORS_MARK_ALL_READ_BUTTON),this.unreadCount=0,this.lastQueried=0,this.userId=this.root.attr("data-userid"),this.container=this.root.find(SELECTORS_ALL_NOTIFICATIONS_CONTAINER),this.limit=20,this.offset=0,this.loadedAll=!1,this.initialLoad=!1,this.unreadCount=this.root.find(SELECTORS_COUNT_CONTAINER).html()};return(NotificationPopoverController.prototype=Object.create(PopoverController.prototype)).constructor=NotificationPopoverController,NotificationPopoverController.prototype.updateButtonAriaLabel=function(){this.isMenuOpen()?Str.get_string("hidenotificationwindow","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):this.unreadCount?Str.get_string("shownotificationwindowwithcount","message",this.unreadCount).done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this)):Str.get_string("shownotificationwindownonew","message").done(function(string){this.menuToggle.attr("aria-label",string)}.bind(this))},NotificationPopoverController.prototype.getContent=function(){return this.container},NotificationPopoverController.prototype.getOffset=function(){return this.offset},NotificationPopoverController.prototype.incrementOffset=function(){this.offset+=this.limit},NotificationPopoverController.prototype.hasDoneInitialLoad=function(){return this.initialLoad},NotificationPopoverController.prototype.hasLoadedAllContent=function(){return this.loadedAll},NotificationPopoverController.prototype.setLoadedAllContent=function(val){this.loadedAll=val},NotificationPopoverController.prototype.renderUnreadCount=function(){var element=this.root.find(SELECTORS_COUNT_CONTAINER);this.unreadCount?(element.text(this.unreadCount),element.removeClass("hidden")):element.addClass("hidden")},NotificationPopoverController.prototype.hideUnreadCount=function(){this.root.find(SELECTORS_COUNT_CONTAINER).addClass("hidden")},NotificationPopoverController.prototype.getNotificationElement=function(id){var element=this.root.find(SELECTORS_NOTIFICATION+'[data-id="'+id+'"]');return 1==element.length?element:null},NotificationPopoverController.prototype.renderNotifications=function(notifications,container){var promises=[];return $.each(notifications,function(index,notification){var offset=this.getOffset()-this.limit;notification.viewmoreurl=URL.relativeUrl("/message/output/popup/notifications.php",{notificationid:notification.id,offset:offset});var notificationurlparams={notificationid:notification.id};notification.contexturl=URL.relativeUrl("message/output/popup/mark_notification_read.php",notificationurlparams);var promise=Templates.render("message_popup/notification_content_item",notification).then((function(html,js){return{html:html,js:js}}));promises.push(promise)}.bind(this)),$.when.apply($,promises).then((function(){$.each(arguments,(function(index,argument){container.append(argument.html),Templates.runTemplateJS(argument.js)}))}))},NotificationPopoverController.prototype.loadMoreNotifications=function(){if(this.isLoading||this.hasLoadedAllContent())return $.Deferred().resolve();this.startLoading();var request={limit:this.limit,offset:this.getOffset(),useridto:this.userId},container=this.getContent();return NotificationRepo.query(request).then(function(result){var notifications=result.notifications;return this.unreadCount=result.unreadcount,this.lastQueried=Math.floor((new Date).getTime()/1e3),this.setLoadedAllContent(!notifications.length||notifications.length{readFeedback.innerHTML=notificationsmarkedasread}))}.bind(this)).always(function(){this.markAllReadButton.removeClass("loading")}.bind(this))},NotificationPopoverController.prototype.registerEventListeners=function(){CustomEvents.define(this.root,[CustomEvents.events.activate]),this.root.on(CustomEvents.events.activate,SELECTORS_MARK_ALL_READ_BUTTON,function(e,data){this.unreadCount>0&&this.markAllAsRead(),e.stopPropagation(),data.originalEvent.preventDefault()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_NOTIFICATION_LINK,function(e){var element=$(e.target).closest(SELECTORS_NOTIFICATION);element.hasClass("unread")&&(this.unreadCount--,element.removeClass("unread")),e.stopPropagation()}.bind(this)),this.root.on(CustomEvents.events.activate,SELECTORS_CLOSE_NOTIFICATION_POPOVER,function(e){e.preventDefault(),$(this.root).trigger(CustomEvents.events.escape),e.stopPropagation()}.bind(this)),this.root.on(this.events().menuOpened,function(){this.hideUnreadCount(),this.updateButtonAriaLabel(),this.hasDoneInitialLoad()||this.loadMoreNotifications();const contentContainer=this.getContentContainer()[0].parentNode;FocusLock.trapFocus(contentContainer)}.bind(this)),this.root.on(this.events().menuClosed,function(){this.renderUnreadCount(),this.updateButtonAriaLabel(),FocusLock.untrapFocus()}.bind(this)),this.root.on(this.events().startLoading,function(){this.getContent().attr("aria-busy","true")}.bind(this)),this.root.on(this.events().stopLoading,function(){this.getContent().attr("aria-busy","false")}.bind(this)),this.getContentContainer().on(CustomEvents.events.scrollBottom,function(){this.isLoading||this.hasLoadedAllContent()||this.loadMoreNotifications()}.bind(this)),CustomEvents.define(this.getContentContainer(),[CustomEvents.events.scrollLock]),$(document).on(NotificationAreaEvents.notificationShown,function(e,notification){if(!notification.read){var element=this.getNotificationElement(notification.id);element&&element.removeClass("unread"),this.unreadCount--,this.renderUnreadCount()}}.bind(this))},NotificationPopoverController})); //# sourceMappingURL=notification_popover_controller.min.js.mapPKS\&<"mark_notification_read.phpnu[. /** * Mark a notification read and redirect to the relevant content. * * @package message_popup * @copyright 2018 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); require_login(null, false); if (isguestuser()) { redirect($CFG->wwwroot); } $notificationid = required_param('notificationid', PARAM_INT); $notification = $DB->get_record('notifications', array('id' => $notificationid)); // If the redirect URL after filtering is empty, or it was never passed, then redirect to the notification page. if (!empty($notification->contexturl)) { $redirecturl = new moodle_url($notification->contexturl); } else { $redirecturl = new moodle_url('/message/output/popup/notifications.php', ['notificationid' => $notificationid]); } // Check notification belongs to this user. if ($USER->id != $notification->useridto) { redirect($CFG->wwwroot); } \core_message\api::mark_notification_as_read($notification); redirect($redirecturl); PKS\`. tests/messaging_cleanup_test.phpnu[. namespace message_popup; use core\task\messaging_cleanup_task; use message_popup_test_helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/message/output/popup/tests/base.php'); /** * Test class * * @package message_popup * @category test * @copyright 2020 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class messaging_cleanup_test extends \advanced_testcase { // Helper trait for sending fake popup notifications. use message_popup_test_helper; /** * Test that all popup notifications are cleaned up * * @return void */ public function test_cleanup_all_notifications(): void { global $DB; $this->resetAfterTest(); $userfrom = $this->getDataGenerator()->create_user(); $userto = $this->getDataGenerator()->create_user(); $now = time(); $this->send_fake_unread_popup_notification($userfrom, $userto, 'Message 1', $now - 10); $notificationid = $this->send_fake_unread_popup_notification($userfrom, $userto, 'Message 2', $now); // Sanity check. $this->assertEquals(2, $DB->count_records('message_popup_notifications')); // Delete all notifications >5 seconds old. set_config('messagingdeleteallnotificationsdelay', 5); (new messaging_cleanup_task())->execute(); // We should have just one record now, matching the second notification we sent. $records = $DB->get_records('message_popup_notifications'); $this->assertCount(1, $records); $this->assertEquals($notificationid, reset($records)->notificationid); } /** * Test that read popup notifications are cleaned up * * @return void */ public function test_cleanup_read_notifications(): void { global $DB; $this->resetAfterTest(); $userfrom = $this->getDataGenerator()->create_user(); $userto = $this->getDataGenerator()->create_user(); $now = time(); $this->send_fake_read_popup_notification($userfrom, $userto, 'Message 1', $now - 20, $now - 10); $notificationid = $this->send_fake_read_popup_notification($userfrom, $userto, 'Message 2', $now - 15, $now); // Sanity check. $this->assertEquals(2, $DB->count_records('message_popup_notifications')); // Delete read notifications >5 seconds old. set_config('messagingdeletereadnotificationsdelay', 5); (new messaging_cleanup_task())->execute(); // We should have just one record now, matching the second notification we sent. $records = $DB->get_records('message_popup_notifications'); $this->assertCount(1, $records); $this->assertEquals($notificationid, reset($records)->notificationid); } } PKS\8 ..tests/api_test.phpnu[. namespace message_popup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/message/tests/messagelib_test.php'); require_once($CFG->dirroot . '/message/output/popup/tests/base.php'); /** * Test message popup API. * * @package message_popup * @category test * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class api_test extends \advanced_testcase { use \message_popup_test_helper; /** @var \phpunit_message_sink message redirection. */ public $messagesink; /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->preventResetByRollback(); // Messaging is not compatible with transactions. $this->messagesink = $this->redirectMessages(); $this->resetAfterTest(); } /** * Test that the get_popup_notifications function will return the correct notifications. */ public function test_message_get_popup_notifications(): void { $sender = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'User1')); $recipient = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'User2')); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 1', 1); $this->send_fake_unread_popup_notification($sender, $recipient, 'Message 2', 2); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 3', 3, 1); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 4', 3, 2); $this->send_fake_unread_popup_notification($sender, $recipient, 'Message 5', 4); $notifications = \message_popup\api::get_popup_notifications($recipient->id); $this->assertEquals($notifications[0]->fullmessage, 'Message 5'); $this->assertEquals($notifications[1]->fullmessage, 'Message 4'); $this->assertEquals($notifications[2]->fullmessage, 'Message 3'); $this->assertEquals($notifications[3]->fullmessage, 'Message 2'); $this->assertEquals($notifications[4]->fullmessage, 'Message 1'); } /** * Test that the get_popup_notifications function works correctly with limiting and offsetting * the result set if requested. */ public function test_message_get_popup_notifications_all_limit_and_offset(): void { $sender = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'User1')); $recipient = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'User2')); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 1', 1); $this->send_fake_unread_popup_notification($sender, $recipient, 'Message 2', 2); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 3', 3, 1); $this->send_fake_read_popup_notification($sender, $recipient, 'Message 4', 3, 2); $this->send_fake_unread_popup_notification($sender, $recipient, 'Message 5', 4); $this->send_fake_unread_popup_notification($sender, $recipient, 'Message 6', 5); $notifications = \message_popup\api::get_popup_notifications($recipient->id, 'DESC', 2, 0); $this->assertEquals($notifications[0]->fullmessage, 'Message 6'); $this->assertEquals($notifications[1]->fullmessage, 'Message 5'); $notifications = \message_popup\api::get_popup_notifications($recipient->id, 'DESC', 2, 2); $this->assertEquals($notifications[0]->fullmessage, 'Message 4'); $this->assertEquals($notifications[1]->fullmessage, 'Message 3'); $notifications = \message_popup\api::get_popup_notifications($recipient->id, 'DESC', 0, 3); $this->assertEquals($notifications[0]->fullmessage, 'Message 3'); $this->assertEquals($notifications[1]->fullmessage, 'Message 2'); $this->assertEquals($notifications[2]->fullmessage, 'Message 1'); } /** * Test count_unread_popup_notifications. */ public function test_message_count_unread_popup_notifications(): void { $sender1 = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'User1')); $sender2 = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'User2')); $recipient1 = $this->getDataGenerator()->create_user(array('firstname' => 'Test3', 'lastname' => 'User3')); $recipient2 = $this->getDataGenerator()->create_user(array('firstname' => 'Test4', 'lastname' => 'User4')); $this->send_fake_unread_popup_notification($sender1, $recipient1); $this->send_fake_unread_popup_notification($sender1, $recipient1); $this->send_fake_unread_popup_notification($sender2, $recipient1); $this->send_fake_unread_popup_notification($sender1, $recipient2); $this->send_fake_unread_popup_notification($sender2, $recipient2); $this->send_fake_unread_popup_notification($sender2, $recipient2); $this->send_fake_unread_popup_notification($sender2, $recipient2); $this->send_fake_unread_popup_notification($sender2, $recipient2); $this->assertEquals(\message_popup\api::count_unread_popup_notifications($recipient1->id), 3); $this->assertEquals(\message_popup\api::count_unread_popup_notifications($recipient2->id), 5); } } PKS\fҎ tests/externallib_test.phpnu[. namespace message_popup; use message_popup_external; use message_popup_test_helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/message/output/popup/externallib.php'); require_once($CFG->dirroot . '/message/output/popup/tests/base.php'); /** * Class for external message popup functions unit tests. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class externallib_test extends \advanced_testcase { use message_popup_test_helper; /** @var \phpunit_message_sink message redirection. */ public $messagesink; /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->preventResetByRollback(); // Messaging is not compatible with transactions. $this->messagesink = $this->redirectMessages(); $this->resetAfterTest(); } /** * Test that get_popup_notifications throws an exception if the user * doesn't exist. */ public function test_get_popup_notifications_no_user_exception(): void { $this->resetAfterTest(true); $this->expectException('moodle_exception'); $result = message_popup_external::get_popup_notifications(-2132131, false, 0, 0); } /** * get_popup_notifications should throw exception if user isn't logged in * user. */ public function test_get_popup_notifications_access_denied_exception(): void { $this->resetAfterTest(true); $sender = $this->getDataGenerator()->create_user(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException('moodle_exception'); $result = message_popup_external::get_popup_notifications($sender->id, false, 0, 0); } /** * get_popup_notifications should return notifications for the recipient. */ public function test_get_popup_notifications_as_recipient(): void { $this->resetAfterTest(true); $sender = $this->getDataGenerator()->create_user(array('firstname' => 'Sendy', 'lastname' => 'Sender')); $recipient = $this->getDataGenerator()->create_user(array('firstname' => 'Recipy', 'lastname' => 'Recipient')); $notificationids = array( $this->send_fake_unread_popup_notification($sender, $recipient), $this->send_fake_unread_popup_notification($sender, $recipient), $this->send_fake_read_popup_notification($sender, $recipient), $this->send_fake_read_popup_notification($sender, $recipient), ); // Confirm that admin has super powers to retrieve any notifications. $this->setAdminUser(); $result = message_popup_external::get_popup_notifications($recipient->id, false, 0, 0); $this->assertCount(4, $result['notifications']); // Check we receive custom data as a unserialisable json. $found = 0; foreach ($result['notifications'] as $notification) { if (!empty($notification->customdata)) { $this->assertObjectHasProperty('datakey', json_decode($notification->customdata)); $found++; } } $this->assertEquals(2, $found); $this->setUser($recipient); $result = message_popup_external::get_popup_notifications($recipient->id, false, 0, 0); $this->assertCount(4, $result['notifications']); } /** * get_popup_notifications result set should work with limit and offset. */ public function test_get_popup_notification_limit_offset(): void { $this->resetAfterTest(true); $sender = $this->getDataGenerator()->create_user(array('firstname' => 'Sendy', 'lastname' => 'Sender')); $recipient = $this->getDataGenerator()->create_user(array('firstname' => 'Recipy', 'lastname' => 'Recipient')); $this->setUser($recipient); $notificationids = array( $this->send_fake_unread_popup_notification($sender, $recipient, 'Notification 1', 1), $this->send_fake_unread_popup_notification($sender, $recipient, 'Notification 2', 2), $this->send_fake_unread_popup_notification($sender, $recipient, 'Notification 3', 3), $this->send_fake_unread_popup_notification($sender, $recipient, 'Notification 4', 4), $this->send_fake_read_popup_notification($sender, $recipient, 'Notification 5', 5), $this->send_fake_read_popup_notification($sender, $recipient, 'Notification 6', 6), $this->send_fake_read_popup_notification($sender, $recipient, 'Notification 7', 7), $this->send_fake_read_popup_notification($sender, $recipient, 'Notification 8', 8), ); $result = message_popup_external::get_popup_notifications($recipient->id, true, 2, 0); $this->assertEquals($result['notifications'][0]->id, $notificationids[7]); $this->assertEquals($result['notifications'][1]->id, $notificationids[6]); $result = message_popup_external::get_popup_notifications($recipient->id, true, 2, 2); $this->assertEquals($result['notifications'][0]->id, $notificationids[5]); $this->assertEquals($result['notifications'][1]->id, $notificationids[4]); } /** * get_unread_popup_notification should throw an exception for an invalid user. */ public function test_get_unread_popup_notification_count_invalid_user_exception(): void { $this->resetAfterTest(true); $this->expectException('moodle_exception'); $result = message_popup_external::get_unread_popup_notification_count(-2132131, 0); } /** * get_unread_popup_notification_count should throw exception if being requested for * non-logged in user. */ public function test_get_unread_popup_notification_count_access_denied_exception(): void { $this->resetAfterTest(true); $sender = $this->getDataGenerator()->create_user(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $this->expectException('moodle_exception'); $result = message_popup_external::get_unread_popup_notification_count($sender->id, 0); } /** * Test get_unread_popup_notification_count. */ public function test_get_unread_popup_notification_count(): void { $this->resetAfterTest(true); $sender1 = $this->getDataGenerator()->create_user(); $sender2 = $this->getDataGenerator()->create_user(); $sender3 = $this->getDataGenerator()->create_user(); $recipient = $this->getDataGenerator()->create_user(); $this->setUser($recipient); $notificationids = array( $this->send_fake_unread_popup_notification($sender1, $recipient, 'Notification', 1), $this->send_fake_unread_popup_notification($sender1, $recipient, 'Notification', 2), $this->send_fake_unread_popup_notification($sender2, $recipient, 'Notification', 3), $this->send_fake_unread_popup_notification($sender2, $recipient, 'Notification', 4), $this->send_fake_unread_popup_notification($sender3, $recipient, 'Notification', 5), $this->send_fake_unread_popup_notification($sender3, $recipient, 'Notification', 6), ); $count = message_popup_external::get_unread_popup_notification_count($recipient->id); $this->assertEquals($count, 6); } } PKS\,u u #tests/behat/behat_message_popup.phpnu[. /** * Behat message popup related steps definitions. * * @package message_popup * @category test * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); /** * Message popup steps definitions. * * @package message_popup * @category test * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_message_popup extends behat_base { /** * Open the notification popover in the nav bar. * * @Given /^I open the notification popover$/ */ public function i_open_the_notification_popover() { $this->execute('behat_general::i_click_on', array("#nav-notification-popover-container [data-region='popover-region-toggle']", 'css_element')); $node = $this->get_selected_node('css_element', '#nav-notification-popover-container [data-region="popover-region-content"]'); $this->ensure_node_is_visible($node); } /** * Open the message popover in the nav bar. * * @Given /^I open the message popover$/ */ public function i_open_the_message_popover() { $this->execute('behat_general::i_click_on', array("#nav-message-popover-container [data-region='popover-region-toggle']", 'css_element')); $node = $this->get_selected_node('css_element', '#nav-message-popover-container [data-region="popover-region-content"]'); $this->ensure_node_is_visible($node); } } PKS\q8E4TT4tests/behat/notification_popover_preferences.featurenu[@core_message @message_popup Feature: Notification popover preferences In order to modify my notification 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@example.com | @javascript Scenario: User navigates to preferences page Given I log in as "user1" And I open the notification popover When I follow "Notification preferences" Then I should see "Notification preferences" PKS\+/tests/behat/notification_popover_unread.featurenu[@core_message @message_popup @javascript Feature: Notification popover unread notifications In order to be kept informed As a user I am notified about relevant events in Moodle Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | # This should generate some notifications And the following "notifications" exist: | subject | userfrom | userto | timecreated | timeread | | Test 01 | student2 | student1 | 1654587996 | null | | Test 02 | student2 | student1 | 1654587997 | null | Scenario: Notification popover shows correct unread count Given I log in as "student1" # Confirm the popover is saying 1 unread notifications. And I should see "2" in the "#nav-notification-popover-container [data-region='count-container']" "css_element" # Open the popover. When I open the notification popover # Confirm the notifications are visible. Then I should see "Test 01" in the "#nav-notification-popover-container" "css_element" And I should see "Test 02" in the "#nav-notification-popover-container" "css_element" @_bug_phantomjs Scenario: Clicking a notification marks it as read Given I log in as "student1" # Open the notifications. When I open the notification popover And I follow "Test 01" And I open the notification popover And I follow "Test 02" # Confirm the count element is hidden (i.e. there are no unread notifications). Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible Scenario: Mark all notifications as read Given I log in as "student1" When I open the notification popover And I click on "Mark all as read" "link" in the "#nav-notification-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 notifications). Then "[data-region='count-container']" "css_element" in the "#nav-notification-popover-container" "css_element" should not be visible PKS\ w w tests/base.phpnu[. /** * Base trait for message popup tests. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use \core_message\tests\helper as testhelper; trait message_popup_test_helper { /** * Send a fake unread popup notification. * * {@link message_send()} does not support transaction, this function will simulate a message * sent from a user to another. We should stop using it once {@link message_send()} will support * transactions. This is not clean at all, this is just used to add rows to the table. * * @param stdClass $userfrom user object of the one sending the message. * @param stdClass $userto user object of the one receiving the message. * @param string $message message to send. * @param int $timecreated time the message was created. * @return int the id of the message */ protected function send_fake_unread_popup_notification(\stdClass $userfrom, \stdClass $userto, string $message = 'Hello world!', int $timecreated = 0): int { global $DB; $id = testhelper::send_fake_unread_notification($userfrom, $userto, $message, $timecreated); $popup = new stdClass(); $popup->notificationid = $id; $DB->insert_record('message_popup_notifications', $popup); return $id; } /** * Send a fake read popup notification. * * {@link message_send()} does not support transaction, this function will simulate a message * sent from a user to another. We should stop using it once {@link message_send()} will support * transactions. This is not clean at all, this is just used to add rows to the table. * * @param stdClass $userfrom user object of the one sending the message. * @param stdClass $userto user object of the one receiving the message. * @param string $message message to send. * @param int $timecreated time the message was created. * @param int $timeread the the message was read * @return int the id of the message */ protected function send_fake_read_popup_notification(\stdClass $userfrom, \stdClass $userto, string $message = 'Hello world!', int $timecreated = 0, int $timeread = 0): int { global $DB; $id = testhelper::send_fake_read_notification($userfrom, $userto, $message, $timecreated, $timeread); $popup = new stdClass(); $popup->notificationid = $id; $DB->insert_record('message_popup_notifications', $popup); return $id; } } PKS\UX__message_output_popup.phpnu[. /** * Popup message processor * * @package message_popup * @copyright 2008 Luis Rodrigues * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later */ require_once(__DIR__ . '/../../../config.php'); //included from messagelib (how to fix?) require_once($CFG->dirroot.'/message/output/lib.php'); /** * The popup message processor * * @package message_popup * @copyright 2008 Luis Rodrigues and Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class message_output_popup extends message_output { /** * Adds notifications to the 'message_popup_notifications' table if applicable. * * The reason for this is because we may not want to show all notifications in the notification popover. This * can happen if the popup processor was disabled when the notification was sent. If the processor is disabled this * function is never called so the notification will never be added to the 'message_popup_notifications' table. * Essentially this table is used to filter what notifications to display from the 'notifications' table. * * @param object $eventdata the event data submitted by the message sender plus $eventdata->savedmessageid * @return true if ok, false if error */ public function send_message($eventdata) { global $DB; // Prevent users from getting popup notifications from themselves (happens with forum notifications). if ($eventdata->notification) { if (($eventdata->userfrom->id != $eventdata->userto->id) || (isset($eventdata->anonymous) && $eventdata->anonymous)) { if (!$DB->record_exists('message_popup_notifications', ['notificationid' => $eventdata->savedmessageid])) { $record = new stdClass(); $record->notificationid = $eventdata->savedmessageid; $DB->insert_record('message_popup_notifications', $record); } } } return true; } /** * Creates necessary fields in the messaging config form. * * @param array $preferences An array of user preferences */ function config_form($preferences) { return null; } /** * Parses the submitted form data and saves it into preferences array. * * @param stdClass $form preferences form class * @param array $preferences preferences array */ public function process_form($form, &$preferences) { return true; } /** * Loads the config data from database to put on the form during initial form display * * @param array $preferences preferences array * @param int $userid the user id */ public function load_data(&$preferences, $userid) { global $USER; return true; } /** * Don't show this processor on the message preferences page. The user can't disable * the notifications for user-to-user messaging. * * @return bool */ public function has_message_preferences() { return false; } /** * Determines if this processor should process a message regardless of user preferences or site settings. * * @return bool */ public function force_process_messages() { global $CFG; return !empty($CFG->messaging); } /** * Remove all popup notifications up to specified time * * @param int $notificationdeletetime * @return void */ public function cleanup_all_notifications(int $notificationdeletetime): void { global $DB; $DB->delete_records_select('message_popup_notifications', 'notificationid IN (SELECT id FROM {notifications} WHERE timecreated < ?)', [$notificationdeletetime]); } /** * Remove read popup notifications up to specified time * * @param int $notificationdeletetime * @return void */ public function cleanup_read_notifications(int $notificationdeletetime): void { global $DB; $DB->delete_records_select('message_popup_notifications', 'notificationid IN (SELECT id FROM {notifications} WHERE timeread < ?)', [$notificationdeletetime]); } } PKS\K0]ԥ notifications.phpnu[. /** * View a user's notifications. * * @package message_popup * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../config.php'); $notificationid = optional_param('notificationid', 0, PARAM_INT); $offset = optional_param('offset', 0, PARAM_INT); $limit = optional_param('limit', 0, PARAM_INT); $userid = $USER->id; $url = new moodle_url('/message/output/popup/notifications.php'); $url->param('id', $notificationid); $PAGE->set_url($url); require_login(); if (isguestuser()) { throw new \moodle_exception('guestnoeditmessage', 'message'); } if (!$user = $DB->get_record('user', ['id' => $userid])) { throw new \moodle_exception('invaliduserid'); } $personalcontext = context_user::instance($user->id); $PAGE->set_context($personalcontext); $PAGE->set_pagelayout('admin'); // Display page header. $title = get_string('notifications', 'message'); $PAGE->set_title($title); $PAGE->set_heading(fullname($user)); // Grab the renderer. $renderer = $PAGE->get_renderer('core', 'message'); $context = [ 'notificationid' => $notificationid, 'userid' => $userid, 'limit' => $limit, 'offset' => $offset, ]; echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('notifications', 'message')); // Display a message if the notifications have not been migrated yet. if (!get_user_preferences('core_message_migrate_data', false, $userid)) { $notify = new \core\output\notification(get_string('notificationdatahasnotbeenmigrated', 'message'), \core\output\notification::NOTIFY_WARNING); echo $OUTPUT->render($notify); } echo $renderer->render_from_template('message_popup/notification_area', $context); echo $OUTPUT->footer(); PKS\Clz''externallib.phpnu[PKS\\ ?EE'db/services.phpnu[PKS\iɛu/db/install.phpnu[PKS\,aaN4db/upgrade.phpnu[PKS\[i:db/install.xmlnu[PKS\Y^*Alang/en/message_popup.phpnu[PKS\V V Elib.phpnu[PKS\^Gǔ Pversion.phpnu[PKS\89Tclasses/api.phpnu[PKS\ɿcdclasses/privacy/provider.phpnu[PKS\Ӻ7 7 %kclasses/output/popup_notification.phpnu[PKS\8utemplates/notification_area_content_area_footer.mustachenu[PKS\)K(8 {templates/notification_area_content_area_header.mustachenu[PKS\ - ,=templates/notification_content_item.mustachenu[PKS\O  $templates/notification_area.mustachenu[PKS\XDö 'templates/message_content_item.mustachenu[PKS\0 'templates/notification_popover.mustachenu[PKS\/9templates/notification_area_content_area_content.mustachenu[PKS\HT  "1amd/src/notification_repository.jsnu[PKS\ÎAA*amd/src/notification_popover_controller.jsnu[PKS\x) amd/src/notification_area_content_area.jsnu[PKS\bb##amd/src/notification_area_events.jsnu[PKS\;5::)z(amd/src/notification_area_control_area.jsnu[PKS\B,camd/build/notification_repository.min.js.mapnu[PKS\5؋)wuamd/build/notification_area_events.min.jsnu[PKS\w w /Twamd/build/notification_area_content_area.min.jsnu[PKS\@[[-*amd/build/notification_area_events.min.js.mapnu[PKS\#$$3amd/build/notification_area_content_area.min.js.mapnu[PKS\B!!(Yamd/build/notification_repository.min.jsnu[PKS\33sPP3ҳamd/build/notification_area_control_area.min.js.mapnu[PKS\Y@]UZUZ4"amd/build/notification_popover_controller.min.js.mapnu[PKS\q/_amd/build/notification_area_control_area.min.jsnu[PKS\30#zamd/build/notification_popover_controller.min.jsnu[PKS\&<"mark_notification_read.phpnu[PKS\`. #tests/messaging_cleanup_test.phpnu[PKS\8 ..Ntests/api_test.phpnu[PKS\fҎ tests/externallib_test.phpnu[PKS\,u u #tests/behat/behat_message_popup.phpnu[PKS\q8E4TT4^tests/behat/notification_popover_preferences.featurenu[PKS\+/tests/behat/notification_popover_unread.featurenu[PKS\ w w otests/base.phpnu[PKS\UX__$ message_output_popup.phpnu[PKS\K0]ԥ notifications.phpnu[PK++(