Jdlrobson has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/348795 )

Change subject: Hygiene: Refactor Notification code - add NotificationBadge 
class
......................................................................

Hygiene: Refactor Notification code - add NotificationBadge class

The code is very brittle here as the NotificationOverlay
communicates with the NotificationBadge. NotificationOverlay assumes
HTML markup rather than uses reliable methods to communicate changes.

This formalises their relationship.

To render the badge on the client the secondaryButton template is
now shipped to the client.

Tests have been added to cover behaviour.
The module lives in skins.minerva.notifications.badge to avoid pulling
all skins.minerva.notifications modules into the environment which can
lead to side effects due to the unconditional wiring up many of these modules
do to the existing HTML. It is not added to mobile.notifications.overlay to
avoid pulling OOjs UI into the startup JavaScript.

Change-Id: I246ed23164ff3f7ef8ea15896a9b3a0d19c2bb0d
---
M extension.json
M includes/Minerva.hooks.php
M includes/MobileFrontend.hooks.php
M includes/skins/SkinMinerva.php
M includes/skins/secondaryButton.mustache
M resources/mobile.notifications.overlay/NotificationsOverlay.js
A resources/skins.minerva.notifications.badge/NotificationBadge.js
M resources/skins.minerva.notifications/init.js
A tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js
9 files changed, 246 insertions(+), 99 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MobileFrontend 
refs/changes/95/348795/1

diff --git a/extension.json b/extension.json
index 622fdc2..8490356 100644
--- a/extension.json
+++ b/extension.json
@@ -1634,6 +1634,7 @@
                        "MobileFrontendHooks::onDiffViewHeader"
                ],
                "ResourceLoaderTestModules": [
+                       "MinervaHooks::onResourceLoaderTestModules",
                        "MobileFrontendHooks::onResourceLoaderTestModules"
                ],
                "GetCacheVaryCookies": [
diff --git a/includes/Minerva.hooks.php b/includes/Minerva.hooks.php
index cacafff..7e4e89e 100644
--- a/includes/Minerva.hooks.php
+++ b/includes/Minerva.hooks.php
@@ -13,6 +13,35 @@
  */
 class MinervaHooks {
        /**
+        * ResourceLoaderTestModules hook handler
+        * @see 
https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules
+        *
+        * @param array $testModules
+        * @param ResourceLoader $resourceLoader
+        * @return bool
+        */
+       public static function onResourceLoaderTestModules( array &$testModules,
+               ResourceLoader &$resourceLoader
+       ) {
+               $testModule = [
+                       'dependencies' => [
+                               'skins.minerva.notifications.badge'
+                       ],
+                       'localBasePath' => dirname( __DIR__ ),
+                       'remoteExtPath' => 'MobileFrontend',
+                       'targets' => [ 'mobile', 'desktop' ],
+                       'scripts' => [
+                               
'tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js'
+                       ],
+               ];
+
+               // Expose templates module
+               $testModules['qunit']["tests.skins.minerva"] = $testModule;
+
+               return true;
+       }
+
+       /**
         * Invocation of hook SpecialPageBeforeExecute
         *
         * We use this hook to ensure that login/account creation pages
diff --git a/includes/MobileFrontend.hooks.php 
b/includes/MobileFrontend.hooks.php
index aca4ee2..9fa1bc5 100644
--- a/includes/MobileFrontend.hooks.php
+++ b/includes/MobileFrontend.hooks.php
@@ -1024,12 +1024,23 @@
                // add Echo, if it's installed
                if ( class_exists( 'MWEchoNotifUser' ) ) {
                        $resourceLoader->register( [
+                               'skins.minerva.notifications.badge' => 
$resourceBoilerplate + [
+                                       'dependencies' => [
+                                               'mediawiki.router',
+                                               'mobile.startup',
+                                       ],
+                                       'scripts' => [
+                                               
'resources/skins.minerva.notifications.badge/NotificationBadge.js',
+                                       ],
+                                       'templates' => [
+                                               'badge.hogan' => 
'includes/skins/secondaryButton.mustache',
+                                       ],
+                                       'targets' => [ 'mobile', 'desktop' ],
+                               ],
                                'skins.minerva.notifications' => 
$resourceBoilerplate + [
                                        'dependencies' => [
-                                               'mobile.startup',
-                                               'mediawiki.router',
+                                               
'skins.minerva.notifications.badge',
                                                'skins.minerva.scripts',
-                                               'mediawiki.ui.anchor'
                                        ],
                                        'scripts' => [
                                                
'resources/skins.minerva.notifications/init.js',
diff --git a/includes/skins/SkinMinerva.php b/includes/skins/SkinMinerva.php
index 627ebcc..6b3b367 100644
--- a/includes/skins/SkinMinerva.php
+++ b/includes/skins/SkinMinerva.php
@@ -382,7 +382,7 @@
                                [ 'returnto' => 
$currentTitle->getPrefixedText() ] );
 
                        $tpl->set( 'secondaryButtonData', [
-                               'class' => MobileUI::iconClass( 'notifications' 
),
+                               'notificationIconClass' => MobileUI::iconClass( 
'notifications' ),
                                'title' => $notificationsMsg,
                                'url' => $url,
                                'notificationCount' => $countLabel,
diff --git a/includes/skins/secondaryButton.mustache 
b/includes/skins/secondaryButton.mustache
index 205554e..b3d6137 100644
--- a/includes/skins/secondaryButton.mustache
+++ b/includes/skins/secondaryButton.mustache
@@ -1,6 +1,6 @@
 {{^hasUnseenNotifications}}
 <a href="{{url}}" title="{{title}}"
-   class="{{class}} user-button main-header-button" id="secondary-button">
+   class="{{notificationIconClass}} user-button main-header-button" 
id="secondary-button">
     {{title}}
 </a>
 {{/hasUnseenNotifications}}
diff --git a/resources/mobile.notifications.overlay/NotificationsOverlay.js 
b/resources/mobile.notifications.overlay/NotificationsOverlay.js
index f55db5a..b3eaaa5 100644
--- a/resources/mobile.notifications.overlay/NotificationsOverlay.js
+++ b/resources/mobile.notifications.overlay/NotificationsOverlay.js
@@ -20,7 +20,7 @@
                Overlay.apply( this, options );
 
                // Anchor tag that corresponds to a notifications badge
-               this.$badge = options.$badge;
+               this.badge = options.badge;
                this.$overlay = $( '<div>' )
                        .addClass( 'notifications-overlay-overlay 
position-fixed' );
 
@@ -32,7 +32,6 @@
 
                mw.echo.config.maxPrioritizedActions = 1;
 
-               this.count = 0;
                this.doneLoading = false;
 
                unreadCounter = new mw.echo.dm.UnreadNotificationCounter( 
echoApi, 'all', maxNotificationCount );
@@ -90,7 +89,7 @@
                wrapperWidget.populate()
                        .then( this.setDoneLoading.bind( this ) )
                        .then( this.controller.updateSeenTime.bind( 
this.controller ) )
-                       .then( this.setBadgeSeen.bind( this ) )
+                       .then( this.badge.markAsSeen() )
                        .then( this.checkShowMarkAllRead.bind( this ) );
        };
 
@@ -159,7 +158,7 @@
                 * @method
                 */
                onError: function () {
-                       window.location.href = this.$badge.attr( 'href' );
+                       window.location.href = this.badge.getNotificationURL();
                },
                /**
                 * Update the unread number on the notifications badge
@@ -168,29 +167,13 @@
                 * @method
                 */
                onUnreadCountChange: function ( count ) {
-                       var $badgeCounter = this.$badge.find( 
'.notification-count' );
-                       this.count = 
this.controller.manager.getUnreadCounter().getCappedNotificationCount( count );
+                       this.badge.setCount(
+                               
this.controller.manager.getUnreadCounter().getCappedNotificationCount( count )
+                       );
 
-                       if ( this.count >= 0 ) {
-                               $badgeCounter.find( 'span' ).text(
-                                       mw.msg( 'echo-badge-count', 
mw.language.convertNumber( this.count ) )
-                               ).show();
-                       }
-                       if ( this.count === 0 ) {
-                               $badgeCounter.removeClass( 
'notification-unseen' );
-                       }
                        this.checkShowMarkAllRead();
                },
-               /**
-                * Mark that all the notifications in the badge are seen.
-                *
-                * @method
-                */
-               setBadgeSeen: function () {
-                       this.$badge
-                               .find( '.notification-count' )
-                               .removeClass( 'notification-unseen' );
-               },
+
                /** @inheritdoc */
                preRender: function () {
                        this.options.heading = '<strong>' + mw.msg( 
'notifications' ) + '</strong>';
diff --git a/resources/skins.minerva.notifications.badge/NotificationBadge.js 
b/resources/skins.minerva.notifications.badge/NotificationBadge.js
new file mode 100644
index 0000000..bec2c28
--- /dev/null
+++ b/resources/skins.minerva.notifications.badge/NotificationBadge.js
@@ -0,0 +1,133 @@
+( function ( M ) {
+       var View = M.require( 'mobile.startup/View' ),
+               Icon = M.require( 'mobile.startup/Icon' ),
+               notificationIcon = new Icon( { name: 'notifications' } ),
+               icons = M.require( 'mobile.startup/icons' );
+
+       /**
+        * A notification button for communicating with an NotificationOverlay
+        * @class NotificationButton
+        * @extends View
+        *
+        * @constructor
+        * @param {Object} options Configuration options
+        */
+       function NotificationBadge( options ) {
+               var $el,
+                       el = options.el;
+
+               if ( el ) {
+                       $el = $( el );
+                       options.hasUnseenNotifications = $el.find( 
'.notification-unseen' ).length;
+                       options.title = $el.find( 'a' ).attr( 'title' );
+                       options.url = $el.find( 'a' ).attr( 'href' );
+               }
+               View.apply( this, arguments );
+               this.url = this.$el.find( 'a' ).attr( 'href' );
+               this._bindOverlayManager();
+       }
+
+       OO.mfExtend( NotificationBadge, View, {
+               /** @inheritdoc */
+               defaults: {
+                       notificationIconClass: notificationIcon.getClassName(),
+                       isLoading: false,
+                       loadingIconHtml: icons.spinner().toHtmlString(),
+                       hasUnseenNotifications: false
+               },
+               /**
+                * Loads a ResourceLoader module script. Shows ajax loader 
whilst loading.
+                * @method
+                * @private
+                * @param {string} moduleName Name of a module to fetch
+                * @return {jQuery.Deferred}
+                */
+               _loadModuleScript: function ( moduleName ) {
+                       var self = this;
+
+                       this.$el.html( this.options.loadingIconHtml );
+                       return mw.loader.using( moduleName ).done( function () {
+                               // trigger a re-render once one to remove 
loading icon
+                               self.render();
+                       } );
+               },
+               /**
+                * Load the notification overlay.
+                * @method
+                * @private
+                * @uses NotificationsOverlay
+                * @return {jQuery.Deferred} with an instance of 
NotificationsOverlay
+                */
+               _loadNotificationOverlay: function () {
+                       var self = this;
+
+                       return this._loadModuleScript( 
'mobile.notifications.overlay' ).then( function () {
+                               var NotificationsOverlay = M.require( 
'mobile.notifications.overlay/NotificationsOverlay' );
+                               return new NotificationsOverlay( {
+                                       badge: self
+                               } );
+                       } );
+               },
+               /**
+                * Sets up routes in overlay manager and click behaviour for 
NotificationBadge
+                * This is not unit tested as it's behaviour is covered by 
browser tests.
+                * @private
+                */
+               _bindOverlayManager: function () {
+                       var self = this,
+                               mainMenu = this.options.mainMenu;
+
+                       this.$el.on( 'click', $.proxy( this.onClickBadge, this 
) );
+                       this.options.overlayManager.add( /^\/notifications$/, 
function () {
+                               return self._loadNotificationOverlay().done( 
function ( overlay ) {
+                                       mainMenu.openNavigationDrawer( 
'secondary' );
+                                       overlay.on( 'hide', function () {
+                                               
mainMenu.closeNavigationDrawers();
+                                               $( '#mw-mf-page-center' ).off( 
'.secondary' );
+                                       } );
+
+                                       $( '#mw-mf-page-center' ).one( 
'click.secondary', function () {
+                                               self.options.router.back();
+                                       } );
+                               } );
+                       } );
+               },
+               /** @inheritdoc */
+               template: mw.template.get( 'skins.minerva.notifications.badge', 
'badge.hogan' ),
+               /**
+                * Click handler for clicking on the badge
+                * @return {Boolean}
+                */
+               onClickBadge: function () {
+                       this.options.router.navigate( '#/notifications' );
+                       // Important that we also prevent propagation to avoid 
interference with events that may be
+                       // binded on #mw-mf-page-center that close overlay
+                       return false;
+               },
+               /**
+                * Return the URL for the full non-overlay notification view
+                * @return {String} url
+                */
+               getNotificationURL: function () {
+                       return this.options.url;
+               },
+               /**
+                * Update the notification count
+                * @param {Number} count
+                */
+               setCount: function ( count ) {
+                       this.options.notificationCount = count;
+                       this.options.isNotificationCountZero = count === 0;
+                       this.render();
+               },
+               /**
+                * Marks all notifications as seen
+                */
+               markAsSeen: function () {
+                       this.options.hasUnseenNotifications = false;
+                       this.render();
+               }
+       } );
+
+       M.define( 'skins.minerva.notifications/NotificationBadge', 
NotificationBadge );
+}( mw.mobileFrontend ) );
diff --git a/resources/skins.minerva.notifications/init.js 
b/resources/skins.minerva.notifications/init.js
index 4b285f1..4cbccb2 100644
--- a/resources/skins.minerva.notifications/init.js
+++ b/resources/skins.minerva.notifications/init.js
@@ -2,81 +2,22 @@
  * This code loads the necessary modules for the notifications overlay, not to 
be confused
  * with the Toast notifications defined by common/toast.js.
  */
-( function ( M, $, mw ) {
+( function ( M, $ ) {
        var mainMenu = M.require( 'skins.minerva.scripts/skin' ).getMainMenu(),
-               $btn = $( '#secondary-button.user-button' ).parent(),
                router = require( 'mediawiki.router' ),
-               overlayManager = M.require( 
'skins.minerva.scripts/overlayManager' ),
-               icons = M.require( 'mobile.startup/icons' );
-
-       /**
-        * Loads a ResourceLoader module script. Shows ajax loader whilst 
loading.
-        * @method
-        * @ignore
-        * FIXME: Upstream to mw.mobileFrontend and reuse elsewhere
-        * @param {string} moduleName Name of a module to fetch
-        * @return {jQuery.Deferred}
-        */
-       function loadModuleScript( moduleName ) {
-               var d = $.Deferred(),
-                       $spinner = $( icons.spinner().toHtmlString() );
-
-               $btn.hide().after( $spinner );
-               mw.loader.using( moduleName, function () {
-                       // FIXME: Some code uses the loading class while other 
code uses the
-                       // spinner class. Make all code consistent so it's 
easier to change.
-                       $spinner.remove();
-                       $btn.show();
-                       d.resolve();
-               } );
-               return d;
-       }
+               NotificationBadge = M.require( 
'skins.minerva.notifications/NotificationBadge' ),
+               overlayManager = M.require( 
'skins.minerva.scripts/overlayManager' );
 
        // Once the DOM is loaded hijack the notifications button to display an 
overlay rather
        // than linking to Special:Notifications.
        $( function () {
-               $btn.on( 'click', function () {
-                       router.navigate( '#/notifications' );
-                       // Important that we also prevent propagation to avoid 
interference with events that may be
-                       // binded on #mw-mf-page-center that close overlay
-                       return false;
-               } );
-
-               /**
-                * Load the notification overlay.
-                * @method
-                * @ignore
-                * @private
-                * @uses NotificationsOverlay
-                * @return {jQuery.Deferred} with an instance of 
NotificationsOverlay
-                */
-               function loadNotificationOverlay() {
-                       var result = $.Deferred();
-                       loadModuleScript( 'mobile.notifications.overlay' 
).done( function () {
-                               var NotificationsOverlay = M.require( 
'mobile.notifications.overlay/NotificationsOverlay' );
-                               result.resolve(
-                                       new NotificationsOverlay( {
-                                               $badge: $btn,
-                                               count: parseInt( $btn.find( 
'span' ).text(), 10 )
-                                       } )
-                               );
-                       } );
-
-                       return result;
-               }
-
-               overlayManager.add( /^\/notifications$/, function () {
-                       return loadNotificationOverlay().done( function ( 
overlay ) {
-                               mainMenu.openNavigationDrawer( 'secondary' );
-                               overlay.on( 'hide', function () {
-                                       mainMenu.closeNavigationDrawers();
-                                       $( '#mw-mf-page-center' ).off( 
'.secondary' );
-                               } );
-
-                               $( '#mw-mf-page-center' ).one( 
'click.secondary', function () {
-                                       router.back();
-                               } );
-                       } );
+               // eslint-disable-next-line no-new
+               new NotificationBadge( {
+                       overlayManager: overlayManager,
+                       router: router,
+                       mainMenu: mainMenu,
+                       el: $( '#secondary-button.user-button' ).parent()
                } );
        } );
-}( mw.mobileFrontend, jQuery, mw ) );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git 
a/tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js 
b/tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js
new file mode 100644
index 0000000..742e669
--- /dev/null
+++ b/tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js
@@ -0,0 +1,49 @@
+( function ( M ) {
+       var OverlayManager = M.require( 'mobile.startup/OverlayManager' ),
+               NotificationBadge = M.require( 
'skins.minerva.notifications/NotificationBadge' );
+
+       QUnit.module( 'Minerva NotificationBadge', {
+               setup: function () {
+                       this.router = require( 'mediawiki.router' );
+                       this.OverlayManager = new OverlayManager( this.router );
+               }
+       } );
+
+       QUnit.test( '#setCount', 2, function ( assert ) {
+               var initialClassExpectationsMet,
+                       badge = new NotificationBadge( {
+                               overlayManager: this.OverlayManager,
+                               hasUnseenNotifications: true,
+                               notificationCount: 5
+                       } );
+               initialClassExpectationsMet = badge.$el.find( '.mw-ui-icon' 
).length === 0 &&
+                       badge.$el.find( '.zero' ).length === 0;
+
+               badge.setCount( 0 );
+               assert.ok( initialClassExpectationsMet, 'No icon and no zero 
class' );
+               assert.ok( badge.$el.find( '.zero' ).length === 1, 'A zero 
class is present on the badge' );
+       } );
+
+       QUnit.test( '#render [hasUnseenNotifications]', 1, function ( assert ) {
+               var badge = new NotificationBadge( {
+                       notificationCount: 0,
+                       overlayManager: this.OverlayManager,
+                       hasUnseenNotifications: false
+               } );
+               assert.ok( badge.$el.find( '.mw-ui-icon' ).length === 1, 'A 
bell icon is visible' );
+       } );
+
+       QUnit.test( '#markAsSeen', 2, function ( assert ) {
+               var badge = new NotificationBadge( {
+                       notificationCount: 2,
+                       overlayManager: this.OverlayManager,
+                       hasUnseenNotifications: true
+               } );
+               // Badge resets counter to zero
+               badge.setCount( 0 );
+               assert.ok( badge.$el.find( '.mw-ui-icon' ).length === 0, 'The 
bell icon is not visible' );
+               badge.markAsSeen();
+               assert.ok( badge.$el.find( '.mw-ui-icon' ).length === 1,
+                       'A bell icon is visible only after a call to markAsSeen 
is called.' );
+       } );
+}( mw.mobileFrontend ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/348795
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I246ed23164ff3f7ef8ea15896a9b3a0d19c2bb0d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/MobileFrontend
Gerrit-Branch: master
Gerrit-Owner: Jdlrobson <jrob...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to