jenkins-bot has submitted this change and it was merged.

Change subject: Initial version of Special:Notifications Javascript page
......................................................................


Initial version of Special:Notifications Javascript page

Bug: T129176
Change-Id: I2f55358c16f78e234ec19154b944a4edebfbe639
---
M Resources.php
M i18n/en.json
M i18n/qqq.json
A images/pending.gif
M includes/special/SpecialNotifications.php
M modules/api/mw.echo.api.EchoApi.js
M modules/api/mw.echo.api.NetworkHandler.js
M modules/controller/mw.echo.Controller.js
M modules/ext.echo.moment-hack.js
M modules/model/mw.echo.dm.ModelManager.js
M modules/model/mw.echo.dm.NotificationsList.js
A modules/model/mw.echo.dm.PaginationModel.js
M modules/nojs/mw.echo.special.less
M modules/special/ext.echo.special.js
A modules/styles/mw.echo.ui.DatedNotificationsWidget.less
A modules/styles/mw.echo.ui.DatedSubGroupListWidget.less
A modules/styles/mw.echo.ui.NotificationsInboxWidget.less
A modules/ui/mw.echo.ui.DatedNotificationsWidget.js
A modules/ui/mw.echo.ui.DatedSubGroupListWidget.js
A modules/ui/mw.echo.ui.NotificationsInboxWidget.js
M modules/ui/mw.echo.ui.SubGroupListWidget.js
21 files changed, 977 insertions(+), 222 deletions(-)

Approvals:
  Catrope: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Resources.php b/Resources.php
index b8b4a5f..7f34ab4 100644
--- a/Resources.php
+++ b/Resources.php
@@ -142,6 +142,8 @@
                        "notification-timestamp-ago-days",
                        "notification-timestamp-ago-months",
                        "notification-timestamp-ago-years",
+                       'notification-timestamp-today',
+                       'notification-timestamp-yesterday',
                        'echo-notification-markasread',
                        'echo-notification-markasunread',
                        'echo-notification-markasread-tooltip',
@@ -161,6 +163,7 @@
                'scripts' => array(
                        'mw.echo.js',
                        'model/mw.echo.dm.js',
+                       'model/mw.echo.dm.PaginationModel.js',
                        'model/mw.echo.dm.ModelManager.js',
                        'model/mw.echo.dm.SortedList.js',
                        'model/mw.echo.dm.NotificationItem.js',
@@ -268,6 +271,30 @@
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
+       'ext.echo.special' => $echoResourceTemplate + array(
+               'scripts' => array(
+                       'ui/mw.echo.ui.DatedSubGroupListWidget.js',
+                       'ui/mw.echo.ui.DatedNotificationsWidget.js',
+                       'ui/mw.echo.ui.NotificationsInboxWidget.js',
+                       'special/ext.echo.special.js',
+               ),
+               'styles' => array(
+                       'styles/mw.echo.ui.DatedSubGroupListWidget.less',
+                       'styles/mw.echo.ui.DatedNotificationsWidget.less',
+                       'styles/mw.echo.ui.NotificationsInboxWidget.less',
+               ),
+               'dependencies' => array(
+                       'ext.echo.ui',
+                       'ext.echo.styles.special'
+               ),
+               'messages' => array(
+                       'echo-load-more-error',
+                       'echo-more-info',
+                       'echo-feedback',
+                       'echo-specialpage-section-markread',
+               ),
+               'targets' => array( 'desktop', 'mobile' ),
+       ),
        'ext.echo.styles.special' => $echoResourceTemplate + array(
                'position' => 'top',
                'styles' => array(
diff --git a/i18n/en.json b/i18n/en.json
index f151c4e..35fbf7a 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -191,6 +191,8 @@
        "notification-timestamp-ago-days": "{{PLURAL:$1|$1d}}",
        "notification-timestamp-ago-months": "{{PLURAL:$1|$1mo}}",
        "notification-timestamp-ago-years": "{{PLURAL:$1|$1yr}}",
+       "notification-timestamp-today": "Today",
+       "notification-timestamp-yesterday": "Yesterday",
        "echo-email-subject-default": "New notification at {{SITENAME}}",
        "echo-email-body-default": "You have a new notification at 
{{SITENAME}}:\n\n$1",
        "echo-email-batch-body-default": "You have a new notification.",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 41e1b6e..1c633fa 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -182,6 +182,8 @@
        "notification-timestamp-ago-days": "Label for the amount of time since 
a notification has arrived in the case where it is in order of days. This 
should be a very short string. $1 - Number of days",
        "notification-timestamp-ago-months": "Label for the amount of time 
since a notification has arrived in the case where it is in order of months. 
This should be a very short string. $1 - Number of months",
        "notification-timestamp-ago-years": "Label for the amount of time since 
a notification has arrived in the case where it is in order of years. This 
should be a very short string. $1 - Number of years",
+       "notification-timestamp-today": "Label for a group of notifications 
that arrived today.",
+       "notification-timestamp-yesterday": "Label for a group of notifications 
that arrived yesterday.",
        "echo-email-subject-default": "Default subject for Echo e-mail 
notifications",
        "echo-email-body-default": "Default message content for Echo email 
notifications. Parameters:\n* $1 - a plain text description of the 
notification",
        "echo-email-batch-body-default": "Default message for Echo e-mail 
digest notifications",
diff --git a/images/pending.gif b/images/pending.gif
new file mode 100644
index 0000000..1194eed
--- /dev/null
+++ b/images/pending.gif
Binary files differ
diff --git a/includes/special/SpecialNotifications.php 
b/includes/special/SpecialNotifications.php
index 9921124..b601c52 100644
--- a/includes/special/SpecialNotifications.php
+++ b/includes/special/SpecialNotifications.php
@@ -127,7 +127,17 @@
                $html .= Html::rawElement( 'ul', array( 'class' => 
'mw-echo-special-notifications' ), $notices );
                $html .= Html::rawElement( 'div', array( 'class' => 
'mw-echo-special-navbar-bottom' ), $navBar );
 
-               $out->addHTML( Html::rawElement( 'div', array( 'class' => 
'mw-echo-special-container' ), $html ) );
+               $html = Html::rawElement( 'div', array( 'class' => 
'mw-echo-special-container' ), $html );
+               $out->addHTML( Html::rawElement( 'div', array( 'class' => 
'mw-echo-special-nojs' ), $html ) );
+
+               $out->addJsConfigVars(
+                       array(
+                               'wgEchoNextContinue' => $nextContinue,
+                       )
+               );
+
+               $out->addModules( array( 'ext.echo.special' ) );
+               // For no-js support
                $out->addModuleStyles( array( 'ext.echo.styles.notifications', 
'ext.echo.styles.special' ) );
        }
 
diff --git a/modules/api/mw.echo.api.EchoApi.js 
b/modules/api/mw.echo.api.EchoApi.js
index ee93eaf..c0de083 100644
--- a/modules/api/mw.echo.api.EchoApi.js
+++ b/modules/api/mw.echo.api.EchoApi.js
@@ -2,12 +2,19 @@
        /**
         * A class defining Echo API instructions and network operations
         *
+        * @class
+        *
         * @constructor
+        * @param {Object} config Configuration options
+        * @cfg {number} [limit=25] Number of notifications to fetch
         */
-       mw.echo.api.EchoApi = function MwEchoApiEchoApi() {
-               this.network = new mw.echo.api.NetworkHandler();
+       mw.echo.api.EchoApi = function MwEchoApiEchoApi( config ) {
+               config = config || {};
+
+               this.network = new mw.echo.api.NetworkHandler( config );
 
                this.fetchingPromise = null;
+               this.limit = config.limit || 25;
        };
 
        OO.initClass( mw.echo.api.EchoApi );
@@ -45,18 +52,29 @@
         * @param {string|string[]} [sources] The source from which to fetch 
the notifications.
         *  If not given, the local notifications will be fetched.
         * @param {boolean} [isForced] Force a refresh on the fetch 
notifications promise
-        * @param {Object} [overrideParams] An object defining parameters to 
override in the API
-        *  fetching call.
+        * @param {string} [continueValue] A value for the continue parameter, 
defining a page
+        * @param {string} [readStatus='all'] Read status of the notifications: 
'read', 'unread' or 'all'
         * @return {jQuery.Promise} Promise that is resolved with all 
notifications for the
         *  requested types.
         */
-       mw.echo.api.EchoApi.prototype.fetchNotifications = function ( type, 
sources, isForced, overrideParams ) {
+       mw.echo.api.EchoApi.prototype.fetchNotifications = function ( type, 
sources, isForced, continueValue, readStatus ) {
+               var overrideParams = {};
                sources = Array.isArray( sources ) ?
                        sources :
                        sources ?
                                [ sources ] :
                                null;
 
+               if ( continueValue ) {
+                       overrideParams.notcontinue = continueValue;
+               }
+
+               if ( readStatus && readStatus !== 'all' ) {
+                       overrideParams.notfilter = readStatus === 'read' ?
+                               'read' :
+                               '!read';
+               }
+
                return this.network.getApiHandler( 'local' 
).fetchNotifications( type, sources, isForced, overrideParams )
                        .then( function ( result ) {
                                return OO.getProp( result.query, 
'notifications' );
diff --git a/modules/api/mw.echo.api.NetworkHandler.js 
b/modules/api/mw.echo.api.NetworkHandler.js
index 81a000f..e8c85f2 100644
--- a/modules/api/mw.echo.api.NetworkHandler.js
+++ b/modules/api/mw.echo.api.NetworkHandler.js
@@ -6,12 +6,16 @@
         * @class
         *
         * @constructor
+        * @param {Object} config Configuration options
+        * @cfg {number} limit Number of notifications to fetch
         */
-       mw.echo.api.NetworkHandler = function MwEchoApiNetworkHandler() {
+       mw.echo.api.NetworkHandler = function MwEchoApiNetworkHandler( config ) 
{
+               config = config || {};
+
                this.handlers = {};
 
                // Add initial local handler
-               this.setApiHandler( 'local', new mw.echo.api.LocalAPIHandler() 
);
+               this.setApiHandler( 'local', new mw.echo.api.LocalAPIHandler( { 
limit: config.limit } ) );
        };
 
        /* Setup */
@@ -63,7 +67,7 @@
        };
 
        /**
-        * Sets an API handler by passing in an instance of an 
mw.echo.api.APIHandler subclass directly.
+        * Set an API handler by passing in an instance of an 
mw.echo.api.APIHandler subclass directly.
         *
         * @param {string} name Symbolic name
         * @param {mw.echo.api.APIHandler} handler Handler object
diff --git a/modules/controller/mw.echo.Controller.js 
b/modules/controller/mw.echo.Controller.js
index adbd5ed..ad497a5 100644
--- a/modules/controller/mw.echo.Controller.js
+++ b/modules/controller/mw.echo.Controller.js
@@ -17,6 +17,118 @@
        OO.initClass( mw.echo.Controller );
 
        /**
+        * Fetch the next page by date
+        *
+        * @return {jQuery.Promise} A promise that resolves with an object 
where the keys are
+        *  days and the items are item IDs.
+        */
+       mw.echo.Controller.prototype.fetchNextPageByDate = function () {
+               this.manager.getPaginationModel().forwards();
+               return this.fetchLocalNotificationsByDate();
+       };
+
+       /**
+        * Fetch the previous page by date
+        *
+        * @return {jQuery.Promise} A promise that resolves with an object 
where the keys are
+        *  days and the items are item IDs.
+        */
+       mw.echo.Controller.prototype.fetchPrevPageByDate = function () {
+               this.manager.getPaginationModel().backwards();
+               return this.fetchLocalNotificationsByDate();
+       };
+
+       /**
+        * Fetch the first page by date
+        *
+        * @return {jQuery.Promise} A promise that resolves with an object 
where the keys are
+        *  days and the items are item IDs.
+        */
+       mw.echo.Controller.prototype.fetchFirstPageByDate = function () {
+               this.manager.getPaginationModel().setCurrPageIndex( 0 );
+               return this.fetchLocalNotificationsByDate();
+       };
+
+       /**
+        * Fetch notifications from the local API and sort them by date.
+        * This method ignores cross-wiki notifications and bundles.
+        *
+        * @param {number} [page] Page number. If not given, it defaults to the 
current
+        *  page.
+        * @return {jQuery.Promise} A promise that resolves with an object 
where the keys are
+        *  days and the items are item IDs.
+        */
+       mw.echo.Controller.prototype.fetchLocalNotificationsByDate = function ( 
page ) {
+               var controller = this,
+                       pagination = this.manager.getPaginationModel(),
+                       continueValue = pagination.getPageContinue( page || 
pagination.getCurrPageIndex() );
+
+               return this.api.fetchNotifications(
+                       this.manager.getTypeString(),
+                       'local',
+                       true,
+                       continueValue
+               )
+                       .then( function ( data ) {
+                               var i, notifData, newNotifData, date, 
itemModel, symbolicName,
+                                       dateItemIds = {},
+                                       dateItems = {},
+                                       models = {};
+
+                               data = data || { list: [] };
+
+                               // Go over the data
+                               for ( i = 0; i < data.list.length; i++ ) {
+                                       notifData = data.list[ i ];
+
+                                       // Collect common data
+                                       newNotifData = 
controller.createNotificationData( notifData );
+                                       if ( notifData.type !== 'foreign' ) {
+                                               date = 
newNotifData.timestamp.substring( 0, 8 );
+                                               newNotifData.source = 'local_' 
+ date;
+
+                                               // Single notifications
+                                               itemModel = new 
mw.echo.dm.NotificationItem(
+                                                       notifData.id,
+                                                       newNotifData
+                                               );
+
+                                               dateItems[ date ] = dateItems[ 
date ] || [];
+                                               dateItems[ date ].push( 
itemModel );
+
+                                               dateItemIds[ date ] = 
dateItemIds[ date ] || [];
+                                               dateItemIds[ date ].push( 
notifData.id );
+                                       }
+                               }
+
+                               // Fill in the models
+                               for ( date in dateItems ) {
+                                       symbolicName = 'local_' + date;
+
+                                       // Set up model
+                                       models[ symbolicName ] = new 
mw.echo.dm.NotificationsList( {
+                                               type: 
controller.manager.getTypes(),
+                                               source: symbolicName,
+                                               title: date,
+                                               timestamp: date
+                                       } );
+
+                                       models[ symbolicName ].setItems( 
dateItems[ date ] );
+                               }
+
+                               // Register local sources
+                               controller.api.registerLocalSources( 
Object.keys( models ) );
+
+                               // Update the manager
+                               controller.manager.setNotificationModels( 
models );
+
+                               // Update the pagination
+                               
controller.manager.getPaginationModel().setNextPageContinue( data.continue );
+
+                               return dateItemIds;
+                       } );
+       };
+       /**
         * Fetch notifications from the local API and update the notifications 
list.
         *
         * @param {boolean} [isForced] Force a renewed fetching promise. If set 
to false, the
@@ -90,15 +202,15 @@
                                        localListModel.addItems( localItems );
 
                                        // Update the controller
-                                       controller.manager.setModels( allModels 
);
+                                       
controller.manager.setNotificationModels( allModels );
 
                                        return idArray;
                                },
                                // Failure
                                function ( errCode, errObj ) {
-                                       if ( !controller.manager.getModel( 
'local' ) ) {
+                                       if ( 
!controller.manager.getNotificationModel( 'local' ) ) {
                                                // Update the controller
-                                               controller.manager.setModels( { 
local: localListModel } );
+                                               
controller.manager.setNotificationModels( { local: localListModel } );
                                        }
                                        return {
                                                errCode: errCode,
@@ -152,7 +264,7 @@
        mw.echo.Controller.prototype.markEntireListModelRead = function ( 
modelName ) {
                var i, items,
                        itemIds = [],
-                       model = this.manager.getModel( modelName || 'local' );
+                       model = this.manager.getNotificationModel( modelName || 
'local' );
 
                if ( !model ) {
                        // Model doesn't exist
@@ -178,7 +290,7 @@
         */
        mw.echo.Controller.prototype.fetchCrossWikiNotifications = function () {
                var controller = this,
-                       xwikiModel = this.manager.getModel( 'xwiki' );
+                       xwikiModel = this.manager.getNotificationModel( 'xwiki' 
);
 
                if ( !xwikiModel ) {
                        // There is no xwiki notifications model, so we can't
@@ -261,7 +373,7 @@
                // Default to true
                isRead = isRead === undefined ? true : isRead;
 
-               this.manager.getModel( modelSource ).findByIds( itemIds 
).forEach( function ( notification ) {
+               this.manager.getNotificationModel( modelSource ).findByIds( 
itemIds ).forEach( function ( notification ) {
                        notification.toggleRead( isRead );
                } );
 
@@ -282,7 +394,7 @@
        mw.echo.Controller.prototype.markCrossWikiItemsRead = function ( 
itemIds, modelSource ) {
                var sourceModel,
                        notifs = [],
-                       xwikiModel = this.manager.getModel( 'xwiki' );
+                       xwikiModel = this.manager.getNotificationModel( 'xwiki' 
);
 
                if ( !xwikiModel ) {
                        return $.Deferred().reject().promise();
@@ -307,7 +419,7 @@
         */
        mw.echo.Controller.prototype.markEntireCrossWikiItemAsRead = function 
() {
                var controller = this,
-                       xwikiModel = this.manager.getModel( 'xwiki' );
+                       xwikiModel = this.manager.getNotificationModel( 'xwiki' 
);
 
                if ( !xwikiModel ) {
                        return $.Deferred().reject().promise();
@@ -348,7 +460,7 @@
         * Remove the entire cross-wiki model.
         */
        mw.echo.Controller.prototype.removeCrossWikiItem = function () {
-               this.manager.removeModel( 'xwiki' );
+               this.manager.removeNotificationModel( 'xwiki' );
        };
 
        /**
diff --git a/modules/ext.echo.moment-hack.js b/modules/ext.echo.moment-hack.js
index b8e6699..b6b68da 100644
--- a/modules/ext.echo.moment-hack.js
+++ b/modules/ext.echo.moment-hack.js
@@ -3,6 +3,7 @@
        'use strict';
 
        var momentOrigLocale = moment.locale();
+
        // Set up new 'short relative time' locale strings for momentjs
        moment.defineLocale( 'echo-shortRelativeTime', {
                relativeTime: function ( number, withoutSuffix, key ) {
@@ -20,7 +21,17 @@
                                yy: 'years'
                        };
                        return mw.msg( 'notification-timestamp-ago-' + keymap[ 
key ], mw.language.convertNumber( number ) );
-               } } );
+               },
+               calendar: {
+                       // Brackets must surround this output, otherwise moment 
thinks
+                       // this is a format string, and replaces all 'm' with 
minutes,
+                       // 's' with seconds, 'd' with days, etc, which is very 
amusing,
+                       // but entirely unhelpful
+                       sameDay: '[' + mw.msg( 'notification-timestamp-today' ) 
+ ']',
+                       lastDay: '[' + mw.msg( 
'notification-timestamp-yesterday' ) + ']',
+                       lastWeek: 'dddd'
+               }
+       } );
        // Reset back to original locale
        moment.locale( momentOrigLocale );
 } )( mediaWiki );
diff --git a/modules/model/mw.echo.dm.ModelManager.js 
b/modules/model/mw.echo.dm.ModelManager.js
index 16c4ca6..5fcc361 100644
--- a/modules/model/mw.echo.dm.ModelManager.js
+++ b/modules/model/mw.echo.dm.ModelManager.js
@@ -36,7 +36,8 @@
                this.types = Array.isArray( this.types ) ?
                        config.type : [ this.types ];
 
-               this.models = {};
+               this.notificationModels = {};
+               this.paginationModel = new mw.echo.dm.PaginationModel();
 
                // Properties
                this.seenTime = mw.config.get( 'wgEchoSeenTime' ) || {};
@@ -49,7 +50,7 @@
 
        /**
         * @event update
-        * @param {Object[]} Current available models
+        * @param {Object[]} Current available notifications
         *
         * The model has been rebuilt or has been updated
         */
@@ -76,12 +77,12 @@
        /* Methods */
 
        /**
-        * Get the models
+        * Get the notifications
         *
-        * @return {Object} Object of models and their symbolic names
+        * @return {Object} Object of notification models and their symbolic 
names
         */
-       mw.echo.dm.ModelManager.prototype.getAllModels = function () {
-               return this.models;
+       mw.echo.dm.ModelManager.prototype.getAllNotificationModels = function 
() {
+               return this.notificationModels;
        };
 
        /**
@@ -94,23 +95,23 @@
         *              ...
         * }
         */
-       mw.echo.dm.ModelManager.prototype.setModels = function ( 
modelDefinitions ) {
+       mw.echo.dm.ModelManager.prototype.setNotificationModels = function ( 
modelDefinitions ) {
                var modelId,
                        localModel;
 
-               this.resetModels();
+               this.resetNotificationModels();
 
                for ( modelId in modelDefinitions ) {
-                       this.models[ modelId ] = modelDefinitions[ modelId ];
+                       this.notificationModels[ modelId ] = modelDefinitions[ 
modelId ];
                }
 
-               localModel = this.getModel( 'local' );
+               localModel = this.getNotificationModel( 'local' );
                if ( localModel ) {
                        localModel.aggregate( { update: 'itemUpdate' } );
                        localModel.connect( this, { itemUpdate: 
'checkLocalUnreadTalk' } );
                }
 
-               this.emit( 'update', this.getAllModels() );
+               this.emit( 'update', this.getAllNotificationModels() );
        };
 
        /**
@@ -119,8 +120,17 @@
         * @param {string} modelName Unique model name
         * @return {mw.echo.dm.SortedList} Notifications model
         */
-       mw.echo.dm.ModelManager.prototype.getModel = function ( modelName ) {
-               return this.models[ modelName ];
+       mw.echo.dm.ModelManager.prototype.getNotificationModel = function ( 
modelName ) {
+               return this.notificationModels[ modelName ];
+       };
+
+       /**
+        * Get the pagination model
+        *
+        * @return {mw.echo.dm.PaginationModel} Pagination model
+        */
+       mw.echo.dm.ModelManager.prototype.getPaginationModel = function () {
+               return this.paginationModel;
        };
 
        /**
@@ -129,8 +139,8 @@
         * @param {string} modelName Symbolic name of the model
         * @fires remove
         */
-       mw.echo.dm.ModelManager.prototype.removeModel = function ( modelName ) {
-               delete this.models[ modelName ];
+       mw.echo.dm.ModelManager.prototype.removeNotificationModel = function ( 
modelName ) {
+               delete this.notificationModels[ modelName ];
                this.emit( 'remove', modelName );
        };
 
@@ -139,13 +149,13 @@
         *
         * @private
         */
-       mw.echo.dm.ModelManager.prototype.resetModels = function () {
+       mw.echo.dm.ModelManager.prototype.resetNotificationModels = function () 
{
                var model;
 
-               for ( model in this.models ) {
-                       if ( this.models.hasOwnProperty( model ) ) {
-                               this.models[ model ].disconnect( this );
-                               delete this.models[ model ];
+               for ( model in this.notificationModels ) {
+                       if ( this.notificationModels.hasOwnProperty( model ) ) {
+                               this.notificationModels[ model ].disconnect( 
this );
+                               delete this.notificationModels[ model ];
                        }
                }
        };
@@ -165,7 +175,7 @@
         * @return {boolean} Local model has unread notifications.
         */
        mw.echo.dm.ModelManager.prototype.hasLocalUnread = function () {
-               var localModel = this.getModel( 'local' ),
+               var localModel = this.getNotificationModel( 'local' ),
                        isUnread = function ( item ) {
                                return !item.isRead();
                        };
@@ -193,7 +203,7 @@
         * @return {boolean} Local model has unread talk page notifications.
         */
        mw.echo.dm.ModelManager.prototype.hasLocalUnreadTalk = function () {
-               var localModel = this.getModel( 'local' ),
+               var localModel = this.getNotificationModel( 'local' ),
                        isUnreadUserTalk = function ( item ) {
                                return !item.isRead() && item.getCategory() === 
'edit-user-talk';
                        };
@@ -210,7 +220,7 @@
         * @return {boolean} The given model has unseen notifications.
         */
        mw.echo.dm.ModelManager.prototype.hasUnseenInModel = function ( modelId 
) {
-               var model = this.getModel( modelId || 'local' );
+               var model = this.getNotificationModel( modelId || 'local' );
 
                return model && model.hasUnseen();
        };
@@ -222,7 +232,7 @@
         */
        mw.echo.dm.ModelManager.prototype.hasUnseenInAnyModel = function () {
                var model,
-                       models = this.getAllModels();
+                       models = this.getAllNotificationModels();
 
                for ( model in models ) {
                        if ( models[ model ].hasUnseen() ) {
@@ -258,7 +268,7 @@
         * @private
         */
        mw.echo.dm.ModelManager.prototype.setLocalModelItemsSeen = function () {
-               var model = this.getModel( 'local' );
+               var model = this.getNotificationModel( 'local' );
 
                model.getItems().forEach( function ( item ) {
                        item.toggleSeen( true );
diff --git a/modules/model/mw.echo.dm.NotificationsList.js 
b/modules/model/mw.echo.dm.NotificationsList.js
index 3fcc293..97f2732 100644
--- a/modules/model/mw.echo.dm.NotificationsList.js
+++ b/modules/model/mw.echo.dm.NotificationsList.js
@@ -115,6 +115,26 @@
        };
 
        /**
+        * Get an array of all items' IDs for a given type
+        *
+        * @param {string} type Notification type
+        * @return {number[]} Item IDs
+        */
+       mw.echo.dm.NotificationsList.prototype.getAllItemIdsByType = function ( 
type ) {
+               var i,
+                       idArray = [],
+                       items = this.getItems();
+
+               for ( i = 0; i < items.length; i++ ) {
+                       if ( items[ i ].getType() === type ) {
+                               idArray.push( items[ i ].getId() );
+                       }
+               }
+
+               return idArray;
+       };
+
+       /**
         * Get the title associated with this list.
         *
         * @return {string} List title
diff --git a/modules/model/mw.echo.dm.PaginationModel.js 
b/modules/model/mw.echo.dm.PaginationModel.js
new file mode 100644
index 0000000..11adfeb
--- /dev/null
+++ b/modules/model/mw.echo.dm.PaginationModel.js
@@ -0,0 +1,143 @@
+( function ( mw ) {
+       /**
+        * Pagination model for echo notifications pages.
+        *
+        * @class
+        *
+        * @constructor
+        * @param {Object} config Configuration object
+        * @cfg {string} [pageNext] The continue value of the next page
+        */
+       mw.echo.dm.PaginationModel = function MwEchoDmPaginationModel( config ) 
{
+               config = config || {};
+
+               this.pagesContinue = [];
+
+               // Set initial page
+               this.currPageIndex = 0;
+               this.pagesContinue[ 0 ] = '';
+
+               // If a next page is given, fill it
+               if ( config.pageNext ) {
+                       this.setPageContinue( 1, config.pageNext );
+               }
+       };
+
+       /* Initialization */
+
+       OO.initClass( mw.echo.dm.PaginationModel );
+
+       /* Methods */
+
+       /**
+        * Set a page index with its 'continue' value, used for API fetching
+        *
+        * @param {number} page Page index
+        * @param {string} continueVal Continue string value
+        */
+       mw.echo.dm.PaginationModel.prototype.setPageContinue = function ( page, 
continueVal ) {
+               if ( continueVal ) {
+                       this.pagesContinue[ page ] = continueVal;
+               }
+       };
+
+       /**
+        * Get the 'continue' value of a certain page
+        *
+        * @param {number} page Page index
+        * @return {string} Continue string value
+        */
+       mw.echo.dm.PaginationModel.prototype.getPageContinue = function ( page 
) {
+               return this.pagesContinue[ page ];
+       };
+
+       /**
+        * Get the current page index
+        *
+        * @return {number} Current page index
+        */
+       mw.echo.dm.PaginationModel.prototype.getCurrPageIndex = function () {
+               return this.currPageIndex;
+       };
+
+       /**
+        * Set the current page index
+        *
+        * @param {number} index Current page index
+        */
+       mw.echo.dm.PaginationModel.prototype.setCurrPageIndex = function ( 
index ) {
+               this.currPageIndex = index;
+       };
+
+       /**
+        * Move forward to the next page
+        */
+       mw.echo.dm.PaginationModel.prototype.forwards = function () {
+               if ( this.hasNextPage() ) {
+                       this.setCurrPageIndex( this.currPageIndex + 1 );
+               }
+       };
+
+       /**
+        * Move backwards to the previous page
+        */
+       mw.echo.dm.PaginationModel.prototype.backwards = function () {
+               if ( this.hasPrevPage() ) {
+                       this.setCurrPageIndex( this.currPageIndex - 1 );
+               }
+       };
+
+       /**
+        * Get the previous page continue value
+        *
+        * @return {string} Previous page continue value
+        */
+       mw.echo.dm.PaginationModel.prototype.getPrevPageContinue = function () {
+               return this.pagesContinue[ this.currPageIndex - 1 ];
+       };
+
+       /**
+        * Get the current page continue value
+        *
+        * @return {string} Current page continue value
+        */
+       mw.echo.dm.PaginationModel.prototype.getCurrPageContinue = function () {
+               return this.pagesContinue[ this.currPageIndex ];
+       };
+
+       /**
+        * Get the next page continue value
+        *
+        * @return {string} Next page continue value
+        */
+       mw.echo.dm.PaginationModel.prototype.getNextPageContinue = function () {
+               return this.pagesContinue[ this.currPageIndex + 1 ];
+       };
+
+       /**
+        * Set the next page continue value
+        *
+        * @param {string} cont Next page continue value
+        */
+       mw.echo.dm.PaginationModel.prototype.setNextPageContinue = function ( 
cont ) {
+               this.pagesContinue[ this.currPageIndex + 1 ] = cont;
+       };
+
+       /**
+        * Check whether a previous page exists
+        *
+        * @return {boolean} Previous page exists
+        */
+       mw.echo.dm.PaginationModel.prototype.hasPrevPage = function () {
+               return this.currPageIndex > 0;
+       };
+
+       /**
+        * Check whether a next page exists
+        *
+        * @return {boolean} Next page exists
+        */
+       mw.echo.dm.PaginationModel.prototype.hasNextPage = function () {
+               return !!this.pagesContinue[ this.currPageIndex + 1 ];
+       };
+} )( mediaWiki );
diff --git a/modules/nojs/mw.echo.special.less 
b/modules/nojs/mw.echo.special.less
index dbe1c9e..a911b19 100644
--- a/modules/nojs/mw.echo.special.less
+++ b/modules/nojs/mw.echo.special.less
@@ -1,10 +1,24 @@
 /* Echo specific CSS */
 @import '../echo.variables';
 
+.client-js .mw-echo-special-nojs {
+       min-height: 5em;
+       /* @embed */
+       background-image: url(../../images/pending.gif);
+
+       .mw-echo-special-container {
+               display: none;
+       }
+}
+
 /* Custom header styling for Vector and Monobook skins */
 .mw-special-Notifications.skin-vector #firstHeading,
 .mw-special-Notifications.skin-monobook #firstHeading {
        max-width: 600px;
+}
+
+.mw-echo-special-markAllReadButton {
+       float: right;
 }
 
 /* Special styles to use if we're converting subtitle links into header icons 
*/
@@ -63,11 +77,8 @@
        font-weight: 800;
 }
 
-.mw-echo-special-container {
+ul.mw-echo-special-notifications {
        max-width: 600px;
-}
-
-.mw-echo-special-notifications {
        list-style: none none;
        padding: 0;
        margin: 0;
@@ -105,7 +116,9 @@
        }
 }
 
-.mw-echo-special-container {
+.mw-echo-special-notifications {
+       overflow-y: auto;
+
        .mw-echo-notification {
                background-color: #F1F1F1;
 
diff --git a/modules/special/ext.echo.special.js 
b/modules/special/ext.echo.special.js
index 953a3da..919d96d 100644
--- a/modules/special/ext.echo.special.js
+++ b/modules/special/ext.echo.special.js
@@ -1,179 +1,34 @@
 ( function ( $, mw ) {
        'use strict';
-       var useLang = mw.config.get( 'wgUserLanguage' );
-
-       /**
-        * @class mw.echo.special
-        * Defines the behavior of the Special:Notifications page
-        *
-        * @singleton
+       /*!
+        * Echo Special:Notifications page initialization
         */
-       mw.echo.special = {
-
-               notcontinue: null,
-               header: '',
-               processing: false,
-
-               /**
-                * Initialize the property in special notification page.
-                *
-                * @method
-                */
-               initialize: function () {
-                       var skin = mw.config.get( 'skin' );
-
-                       // Convert more link into a button
-                       $( '#mw-echo-more' )
-                               .click( function ( e ) {
-                                       e.preventDefault();
-                                       if ( !mw.echo.special.processing ) {
-                                               mw.echo.special.processing = 
true;
-                                               mw.echo.special.loadMore();
-                                       }
+       $( document ).ready( function () {
+               var limitNotifications = 50,
+                       $content = $( '#mw-content-text' ),
+                       echoApi = new mw.echo.api.EchoApi( { limit: 
limitNotifications } ),
+                       unreadCounter = new 
mw.echo.dm.UnreadNotificationCounter( echoApi, [ 'message', 'alert' ], 
limitNotifications ),
+                       modelManager = new mw.echo.dm.ModelManager( 
unreadCounter, { type: [ 'message', 'alert' ] } ),
+                       controller = new mw.echo.Controller(
+                               echoApi,
+                               modelManager,
+                               {
+                                       type: [ 'message' ]
+                               }
+                       ),
+                       specialPageContainer = new 
mw.echo.ui.NotificationsInboxWidget(
+                               controller,
+                               modelManager,
+                               {
+                                       pageNext: mw.config.get( 
'wgEchoNextContinue' ),
+                                       limit: limitNotifications,
+                                       $overlay: mw.echo.ui.$overlay
                                }
                        );
-                       mw.echo.special.notcontinue = mw.config.get( 
'wgEchoNextContinue' );
-                       mw.echo.special.header = mw.config.get( 
'wgEchoDateHeader' );
 
-                       // Set up each individual notification with 
eventlogging, a close
-                       // box and dismiss interface if it is dismissable.
-                       $( '.mw-echo-notification' ).each( function () {
-                               mw.echo.logger.logInteraction(
-                                       'notification-impression',
-                                       mw.echo.Logger.static.context.archive,
-                                       Number( $( this ).attr( 
'data-notification-event' ) ),
-                                       $( this ).attr( 
'data-notification-type' )
-                               );
-                       } );
+               // Overlay
+               $( 'body' ).append( mw.echo.ui.$overlay );
 
-                       $( '#mw-echo-moreinfo-link' ).click( function () {
-                               mw.echo.logger.logInteraction( 'ui-help-click', 
mw.echo.Logger.static.context.archive );
-                       } );
-                       $( '#mw-echo-pref-link' ).click( function () {
-                               mw.echo.logger.logInteraction( 
'ui-prefs-click', mw.echo.Logger.static.context.archive );
-                       } );
-
-                       // Convert subtitle links into header icons for Vector 
and Monobook skins
-                       if ( skin === 'vector' || skin === 'monobook' ) {
-                               $( '#mw-echo-moreinfo-link, #mw-echo-pref-link' 
)
-                                       .empty()
-                                       .appendTo( '#firstHeading' );
-                               $( '#contentSub' ).empty();
-                       }
-
-               },
-
-               /**
-                * Load more notification records.
-                *
-                * @method
-                */
-               loadMore: function () {
-                       var notifications, container, $li,
-                               api = new mw.Api( { ajax: { cache: false } } ),
-                               seenTime = mw.config.get( 'wgEchoSeenTime' ),
-                               that = this,
-                               unread = [],
-                               apiData = {
-                                       action: 'query',
-                                       meta: 'notifications',
-                                       notformat: 'special',
-                                       notprop: 'list',
-                                       notcontinue: this.notcontinue,
-                                       notlimit: mw.config.get( 
'wgEchoDisplayNum' ),
-                                       uselang: useLang
-                               };
-
-                       api.get( apiData ).done( function ( result ) {
-                               container = $( '#mw-echo-special-container' );
-                               notifications = result.query.notifications;
-                               unread = [];
-
-                               $.each( notifications.list, function ( index, 
data ) {
-                                       if ( that.header !== 
data.timestamp.date ) {
-                                               that.header = 
data.timestamp.date;
-                                               $( '<li></li>' ).addClass( 
'mw-echo-date-section' ).append( that.header ).appendTo( container );
-                                       }
-
-                                       $li = $( '<li></li>' )
-                                               .data( 'details', data )
-                                               .data( 'id', data.id )
-                                               .addClass( 
'mw-echo-notification' )
-                                               .attr( {
-                                                       
'data-notification-category': data.category,
-                                                       
'data-notification-event': data.id,
-                                                       
'data-notification-type': data.type
-                                               } )
-                                               .append( data[ '*' ] )
-                                               .appendTo( container );
-
-                                       if ( !data.read ) {
-                                               $li.addClass( 'mw-echo-unread' 
);
-                                               unread.push( data.id );
-                                       }
-
-                                       if ( seenTime !== null && 
data.timestamp.mw > seenTime ) {
-                                               $li.addClass( 'mw-echo-unseen' 
);
-                                       }
-
-                                       mw.echo.logger.logInteraction(
-                                               'notification-impression',
-                                               
mw.echo.Logger.static.context.archive,
-                                               Number( $li.attr( 
'data-notification-event' ) ),
-                                               $li.attr( 
'data-notification-type' )
-                                       );
-                               } );
-
-                               that.notcontinue = notifications[ 'continue' ];
-                               if ( unread.length > 0 ) {
-                                       that.markAsRead( unread );
-                               } else {
-                                       that.onSuccess();
-                               }
-                       } ).fail( function () {
-                               that.onError();
-                       } );
-               },
-
-               /**
-                * Mark notifications as read.
-                */
-               markAsRead: function ( unread ) {
-                       var api = new mw.Api(),
-                               that = this;
-                       api.postWithToken( 'csrf', {
-                               action: 'echomarkread',
-                               list: unread.join( '|' ),
-                               uselang: useLang
-                       } ).done( function () {
-                               // HACK: We should really redo the way the 
entire special
-                               // page handles the notifications now that they 
are separated
-                               // into 'alert' and 'messages'. However, until 
that happens,
-                               // the badges should be updated individually.
-                               // Don't try this at home.
-                               
mw.echo.ui.messageWidget.fetchUnreadCountFromApi();
-                               
mw.echo.ui.alertWidget.fetchUnreadCountFromApi();
-
-                               that.onSuccess();
-                       } ).fail( function () {
-                               that.onError();
-                       } );
-               },
-
-               onSuccess: function () {
-                       if ( !this.notcontinue ) {
-                               $( '#mw-echo-more' ).hide();
-                       }
-                       this.processing = false;
-               },
-
-               onError: function () {
-                       // Todo: Show detail error message based on error code
-                       $( '#mw-echo-more' ).text( mw.msg( 
'echo-load-more-error' ) );
-                       this.processing = false;
-               }
-       };
-
-       $( document ).ready( mw.echo.special.initialize );
-
+               $content.empty().append( specialPageContainer.$element );
+       } );
 } )( jQuery, mediaWiki );
diff --git a/modules/styles/mw.echo.ui.DatedNotificationsWidget.less 
b/modules/styles/mw.echo.ui.DatedNotificationsWidget.less
new file mode 100644
index 0000000..20acc28
--- /dev/null
+++ b/modules/styles/mw.echo.ui.DatedNotificationsWidget.less
@@ -0,0 +1,14 @@
+.mw-echo-ui-datedNotificationsWidget {
+       min-height: 5em;
+
+       .mw-echo-ui-subGroupListWidget {
+               // This is a hack to make sure that this widget
+               // is the nearest scrollable widget for the submenus
+               overflow-y: auto;
+
+               &-header {
+                       border-bottom: 1px #ccc solid;
+                       margin-bottom: 0.5em;
+               }
+       }
+}
diff --git a/modules/styles/mw.echo.ui.DatedSubGroupListWidget.less 
b/modules/styles/mw.echo.ui.DatedSubGroupListWidget.less
new file mode 100644
index 0000000..841d5fa
--- /dev/null
+++ b/modules/styles/mw.echo.ui.DatedSubGroupListWidget.less
@@ -0,0 +1,15 @@
+.mw-echo-ui-datedSubGroupListWidget {
+       &-title {
+               font-weight: normal;
+
+               &-primary {
+                       font-size: 1.5em;
+                       margin-right: 0.5em;
+               }
+
+               &-secondary {
+                       font-size: 1.5em;
+                       color: #cccccc;
+               }
+       }
+}
diff --git a/modules/styles/mw.echo.ui.NotificationsInboxWidget.less 
b/modules/styles/mw.echo.ui.NotificationsInboxWidget.less
new file mode 100644
index 0000000..b9dbe44
--- /dev/null
+++ b/modules/styles/mw.echo.ui.NotificationsInboxWidget.less
@@ -0,0 +1,66 @@
+.mw-echo-ui-notificationsInboxWidget {
+       &.oo-ui-pendingElement-pending .mw-echo-ui-datedSubGroupListWidget {
+               opacity: 0.5;
+       }
+
+       &-toolbar {
+               &-row {
+                       display: table-row;
+               }
+
+               &-top {
+                       display: table;
+                       margin-bottom: 2em;
+
+
+                       &-placeholder {
+                               display: table-cell;
+                               width: 100%;
+                       }
+               }
+
+               &-bottom {
+                       display: table;
+                       width: inherit;
+                       margin-left: auto;
+                       margin-right: auto;
+                       margin-top: 2em;
+               }
+
+               &-pagination {
+                       &-buttons,
+                       &-start,
+                       &-label {
+                               display: table-cell;
+                       }
+
+                       &-buttons {
+                               vertical-align: middle;
+
+                               .oo-ui-buttonOptionWidget 
.oo-ui-buttonElement-button {
+                                       // This is needed so the height of the 
buttons with icons
+                                       // is the same as the height of the 
button with label text
+                                       // See 
https://phabricator.wikimedia.org/T136024
+                                       height: 1.6em;
+
+                                       .oo-ui-iconElement-icon {
+                                               top: 0.4em;
+                                       }
+                               }
+                       }
+
+                       &-start {
+                               vertical-align: middle;
+                       }
+
+                       &-label {
+                               padding: 0 0.5em;
+                               vertical-align: middle;
+                               span {
+                                       white-space: nowrap;
+                               }
+                       }
+               }
+
+       }
+}
diff --git a/modules/ui/mw.echo.ui.DatedNotificationsWidget.js 
b/modules/ui/mw.echo.ui.DatedNotificationsWidget.js
new file mode 100644
index 0000000..b61efdd
--- /dev/null
+++ b/modules/ui/mw.echo.ui.DatedNotificationsWidget.js
@@ -0,0 +1,158 @@
+( function ( $, mw ) {
+       /**
+        * A notifications list organized and separated by dates
+        *
+        * @class
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.echo.Controller} controller Echo controller
+        * @param {mw.echo.dm.ModelManager} modelManager Model manager
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] An overlay for the popup menus
+        */
+       mw.echo.ui.DatedNotificationsWidget = function 
MwEchoUiDatedNotificationsListWidget( controller, modelManager, config ) {
+               config = config || {};
+
+               // Parent constructor
+               mw.echo.ui.DatedNotificationsWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.PendingElement.call( this, config );
+
+               this.manager = modelManager;
+               this.controller = controller;
+               this.models = {};
+
+               this.$overlay = config.$overlay || this.$element;
+
+               this.listWidget = new mw.echo.ui.SortedListWidget(
+                       // Sorting callback
+                       function ( a, b ) {
+                               // Reverse sorting
+                               return Number( b.getTimestamp() ) - Number( 
a.getTimestamp() );
+                       },
+                       // Config
+                       {
+                               classes: [ 
'mw-echo-ui-datedNotificationsWidget-group' ],
+                               $overlay: this.$overlay
+                       }
+               );
+
+               // Events
+               this.manager.connect( this, {
+                       update: 'populateFromModel',
+                       removeSource: 'onModelRemoveSource'
+               } );
+
+               this.$element
+                       .addClass( 'mw-echo-ui-datedNotificationsWidget' )
+                       .append( this.listWidget.$element );
+
+               // Initialization
+               this.populateFromModel();
+       };
+       /* Initialization */
+
+       OO.inheritClass( mw.echo.ui.DatedNotificationsWidget, OO.ui.Widget );
+       OO.mixinClass( mw.echo.ui.DatedNotificationsWidget, 
OO.ui.mixin.PendingElement );
+
+       /**
+        * Respond to model removing source group
+        *
+        * @param {string} source Symbolic name of the source group
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.onModelRemoveSource = 
function ( source ) {
+               var list = this.getList(),
+                       group = list.getItemFromId( source );
+
+               list.removeItems( [ group ] );
+       };
+
+       /**
+        * Respond to model manager update event.
+        * This event means we are repopulating the entire list and the
+        * associated models within it.
+        *
+        * @param {Object} [models] Object of new models to populate the
+        *  list. If not given, the method will request all models from the
+        *  manager.
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.populateFromModel = 
function ( models ) {
+               var modelId, model, subgroupWidget,
+                       groupWidgets = [];
+
+               models = models || this.manager.getAllNotificationModels();
+
+               // Detach all attached models
+               for ( modelId in this.models ) {
+                       this.detachModel( modelId );
+               }
+
+               for ( model in models ) {
+                       // Create SubGroup widgets
+                       subgroupWidget = new mw.echo.ui.DatedSubGroupListWidget(
+                               this.controller,
+                               models[ model ],
+                               {
+                                       showTitle: true,
+                                       showMarkAllRead: true,
+                                       $overlay: this.$overlay
+                               }
+                       );
+                       subgroupWidget.resetItemsFromModel();
+                       groupWidgets.push( subgroupWidget );
+               }
+
+               // Reset the list and re-add the items
+               this.getList().clearItems();
+               this.getList().addItems( groupWidgets );
+       };
+
+       /**
+        * Attach a model to the widget
+        *
+        * @param {string} modelId Symbolic name for the model
+        * @param {mw.echo.dm.SortedList} model Notifications list model
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.attachModel = function ( 
modelId, model ) {
+               this.models[ modelId ] = model;
+       };
+
+       /**
+        * Detach a model from the widget
+        *
+        * @param {string} modelId Notifications list model
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.detachModel = function ( 
modelId ) {
+               this.models[ modelId ].disconnect( this );
+               delete this.models[ modelId ];
+       };
+
+       /**
+        * Get the list widget contained in this item
+        *
+        * @return {mw.echo.ui.SortedListWidget} List widget
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.getList = function () {
+               return this.listWidget;
+       };
+
+       /**
+        * Get the number of all notifications in all sections of the widget
+        *
+        * @return {number} The number of all notifications
+        */
+       mw.echo.ui.DatedNotificationsWidget.prototype.getAllNotificationCount = 
function () {
+               var i,
+                       count = 0,
+                       groups = this.getList().getItems();
+
+               for ( i = 0; i < groups.length; i++ ) {
+                       count += groups[ i ].getListWidget().getItemCount();
+               }
+
+               return count;
+       };
+
+} )( jQuery, mediaWiki );
diff --git a/modules/ui/mw.echo.ui.DatedSubGroupListWidget.js 
b/modules/ui/mw.echo.ui.DatedSubGroupListWidget.js
new file mode 100644
index 0000000..2b9455f
--- /dev/null
+++ b/modules/ui/mw.echo.ui.DatedSubGroupListWidget.js
@@ -0,0 +1,50 @@
+( function ( mw, $ ) {
+       /*global moment:false */
+       /**
+        * A sub group widget that displays notifications divided by dates.
+        *
+        * @class
+        * @extends mw.echo.ui.SubGroupListWidget
+        *
+        * @constructor
+        * @param {mw.echo.Controller} controller Notifications controller
+        * @param {mw.echo.dm.SortedList} listModel Notifications list model 
for this source
+        * @param {Object} [config] Configuration object
+        */
+       mw.echo.ui.DatedSubGroupListWidget = function 
MwEchoUiDatedSubGroupListWidget( controller, listModel, config ) {
+               var momentTimestamp, diff, fullDate,
+                       now = moment(),
+                       $primaryDate = $( '<span>' )
+                               .addClass( 
'mw-echo-ui-datedSubGroupListWidget-title-primary' ),
+                       $secondaryDate = $( '<span>' )
+                               .addClass( 
'mw-echo-ui-datedSubGroupListWidget-title-secondary' ),
+                       $title = $( '<span>' )
+                               .addClass( 
'mw-echo-ui-datedSubGroupListWidget-title' )
+                               .append( $primaryDate, $secondaryDate );
+
+               config = config || {};
+
+               // Parent constructor
+               mw.echo.ui.DatedSubGroupListWidget.parent.call( this, 
controller, listModel, config );
+
+               momentTimestamp = moment( this.model.getTimestamp(), 'YYYYMMDD' 
);
+               diff = now.diff( momentTimestamp, 'weeks' );
+               fullDate = momentTimestamp.format( 'LL' );
+
+               $primaryDate.text( fullDate );
+               if ( diff === 0 ) {
+                       $secondaryDate.text( fullDate );
+                       momentTimestamp.locale( 'echo-shortRelativeTime' );
+                       $primaryDate.text( momentTimestamp.calendar() );
+               }
+
+               this.title.setLabel( $title );
+
+               this.$element
+                       .addClass( 'mw-echo-ui-datedSubGroupListWidget' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.echo.ui.DatedSubGroupListWidget, 
mw.echo.ui.SubGroupListWidget );
+} )( mediaWiki, jQuery );
diff --git a/modules/ui/mw.echo.ui.NotificationsInboxWidget.js 
b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js
new file mode 100644
index 0000000..c04f97e
--- /dev/null
+++ b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js
@@ -0,0 +1,224 @@
+( function ( $, mw ) {
+       /**
+        * An inbox-type widget that encompases a dated notifications list with 
pagination
+        *
+        * @class
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.echo.Controller} controller Echo controller
+        * @param {mw.echo.dm.ModelManager} manager Model manager
+        * @param {Object} [config] Configuration object
+        * @cfg {number} [limit=25] Limit the number of notifications per page
+        * @cfg {jQuery} [$overlay] An overlay for the popup menus
+        */
+       mw.echo.ui.NotificationsInboxWidget = function 
MwEchoUiNotificationsInboxWidget( controller, manager, config ) {
+               config = config || {};
+
+               // Parent
+               mw.echo.ui.NotificationsInboxWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.PendingElement.call( this, config );
+
+               this.controller = controller;
+               this.manager = manager;
+
+               this.$overlay = config.$overlay || this.$element;
+               this.limit = config.limit || 25;
+
+               // Notifications list
+               this.datedListWidget = new mw.echo.ui.DatedNotificationsWidget(
+                       this.controller,
+                       this.manager,
+                       {
+                               $overlay: this.$overlay
+                       }
+               );
+
+               // Pagination
+               // TODO: Separate the pagination controls and labels to
+               // its own widget
+               // Top
+               this.topPaginationLabel = new OO.ui.LabelWidget();
+               this.topPaginationStart = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'notification-timestamp-today' ),
+                       data: 'start'
+               } );
+               this.topPaginationButtons = this.createPaginationButtons();
+
+               // Bottom
+               this.bottomPaginationLabel = new OO.ui.LabelWidget();
+               this.bottomPaginationStart = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'notification-timestamp-today' ),
+                       data: 'start'
+               } );
+               this.bottomPaginationButtons = this.createPaginationButtons();
+
+               // Events
+               this.topPaginationButtons.connect( this, { choose: 
'onPaginationChoose' } );
+               this.bottomPaginationButtons.connect( this, { choose: 
'onPaginationChoose' } );
+               this.topPaginationStart.connect( this, { click: 
'onPaginationStart' } );
+               this.bottomPaginationStart.connect( this, { click: 
'onPaginationStart' } );
+               this.manager.connect( this, { update: 'updatePaginationLabel' } 
);
+
+               this.disablePagination();
+               // Initialization
+               this.$element
+                       .addClass( 'mw-echo-ui-notificationsInboxWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 
'mw-echo-ui-notificationsInboxWidget-toolbar-top' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 
'mw-echo-ui-notificationsInboxWidget-toolbar-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-top-placeholder' ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-label' )
+                                                                       
.append( this.topPaginationLabel.$element ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-start' )
+                                                                       
.append( this.topPaginationStart.$element ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-buttons' )
+                                                                       
.append( this.topPaginationButtons.$element )
+                                                       )
+                                       ),
+                               this.datedListWidget.$element,
+                               $( '<div>' )
+                                       .addClass( 
'mw-echo-ui-notificationsInboxWidget-toolbar-bottom' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 
'mw-echo-ui-notificationsInboxWidget-toolbar-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-label' )
+                                                                       
.append( this.bottomPaginationLabel.$element ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-start' )
+                                                                       
.append( this.bottomPaginationStart.$element ),
+                                                               $( '<div>' )
+                                                                       
.addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination-buttons' )
+                                                                       
.append( this.bottomPaginationButtons.$element )
+                                                       )
+                                       )
+                       );
+
+               this.populateNotifications();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.echo.ui.NotificationsInboxWidget, OO.ui.Widget );
+       OO.mixinClass( mw.echo.ui.NotificationsInboxWidget, 
OO.ui.mixin.PendingElement );
+
+       /* Methods */
+
+       /**
+        * Respond to pagination start button click event
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.onPaginationStart = 
function () {
+               this.populateNotifications( 'start' );
+       };
+
+       /**
+        * Respond to pagination choose event
+        *
+        * @param {OO.ui.ButtonOptionWidget} item Chosen item
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.onPaginationChoose = 
function ( item ) {
+               var direction = item && item.getData();
+
+               if ( direction ) {
+                       this.populateNotifications( direction );
+                       item.setSelected( false );
+               }
+       };
+
+       /**
+        * Create a set of pagination buttons
+        *
+        * @return {OO.ui.ButtonSelectWidget} Pagination button select widget
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.createPaginationButtons = 
function () {
+               return new OO.ui.ButtonSelectWidget( {
+                       classes: [ 
'mw-echo-ui-notificationsInboxWidget-pagination' ],
+                       items: [
+                               new OO.ui.ButtonOptionWidget( {
+                                       icon: 'previous',
+                                       data: 'prev'
+                               } ),
+                               new OO.ui.ButtonOptionWidget( {
+                                       icon: 'next',
+                                       data: 'next'
+                               } )
+                       ]
+               } );
+       };
+
+       /**
+        * Toggle the pagination. If false, the pagination buttons will be
+        * enabled depending on whether they are a valid action.
+        *
+        * @param {boolean} [isDisabled=true] Pagination is disabled
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.disablePagination = 
function ( isDisabled ) {
+               var pagination = this.manager.getPaginationModel();
+               isDisabled = isDisabled === undefined ? true : isDisabled;
+
+               this.topPaginationButtons.getItemFromData( 'prev' 
).setDisabled( isDisabled || !pagination.hasPrevPage() );
+               this.topPaginationButtons.getItemFromData( 'next' 
).setDisabled( isDisabled || !pagination.hasNextPage() );
+               this.bottomPaginationButtons.getItemFromData( 'prev' 
).setDisabled( isDisabled || !pagination.hasPrevPage() );
+               this.bottomPaginationButtons.getItemFromData( 'next' 
).setDisabled( isDisabled || !pagination.hasNextPage() );
+
+               this.topPaginationStart.toggle( !isDisabled && 
pagination.getCurrPageIndex() >= 2 );
+               this.bottomPaginationStart.toggle( !isDisabled && 
pagination.getCurrPageIndex() >= 2 );
+
+               this.topPaginationLabel.toggle( !isDisabled );
+               this.bottomPaginationLabel.toggle( !isDisabled );
+       };
+
+       /**
+        * Populate the notifications list
+        *
+        * @param {string} [direction] Direction to fetch from. 'prev' for 
previous page
+        *  or 'next' for the next page. If not given, the first page of 
results will be fetched.
+        * @return {jQuery.Promise} A promise that is resolved when the results
+        *  have been fetched.
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.populateNotifications = 
function ( direction ) {
+               var fetchPromise;
+
+               if ( direction === 'prev' ) {
+                       fetchPromise = this.controller.fetchPrevPageByDate();
+               } else if ( direction === 'next' ) {
+                       fetchPromise = this.controller.fetchNextPageByDate();
+               } else {
+                       fetchPromise = this.controller.fetchFirstPageByDate();
+               }
+
+               this.pushPending();
+               this.disablePagination();
+               return fetchPromise
+                       // Re-enable pagination
+                       .then( this.disablePagination.bind( this, false ) )
+                       // Pop pending
+                       .always( this.popPending.bind( this ) );
+       };
+
+       /**
+        * Update the pagination label according to the page number, the amount 
of notifications
+        * per page, and the amount of notifications on the current page.
+        */
+       mw.echo.ui.NotificationsInboxWidget.prototype.updatePaginationLabel = 
function () {
+               var firstNotifNum = ( 
this.manager.getPaginationModel().getCurrPageIndex() * this.limit ),
+                       lastNotifNum = firstNotifNum + 
this.datedListWidget.getAllNotificationCount(),
+                       label = ( firstNotifNum + 1 ) + ' - ' + lastNotifNum;
+
+               // Display the range
+               this.topPaginationLabel.setLabel( label );
+               this.bottomPaginationLabel.setLabel( label );
+       };
+} )( jQuery, mediaWiki );
diff --git a/modules/ui/mw.echo.ui.SubGroupListWidget.js 
b/modules/ui/mw.echo.ui.SubGroupListWidget.js
index c823fba..e0f772b 100644
--- a/modules/ui/mw.echo.ui.SubGroupListWidget.js
+++ b/modules/ui/mw.echo.ui.SubGroupListWidget.js
@@ -69,7 +69,8 @@
                // Mark all as read button
                this.markAllReadButton = new OO.ui.ButtonWidget( {
                        framed: true,
-                       label: mw.msg( 'echo-mark-all-as-read' ),
+                       icon: 'doubleCheck',
+                       label: mw.msg( 'echo-specialpage-section-markread' ),
                        classes: [ 
'mw-echo-ui-subGroupListWidget-header-row-markAllReadButton' ]
                } );
 
@@ -84,11 +85,11 @@
                        // Update all items
                        update: 'resetItemsFromModel'
                } );
-               this.markAllReadButton.connect( this, { click: 
'onMarkAllReadButtonClick' } );
                // We must aggregate on item update, so we know when and if all
                // items are read and can hide/show the 'mark all read' button
                this.model.aggregate( { update: 'itemUpdate' } );
                this.model.connect( this, { itemUpdate: 
'toggleMarkAllReadButton' } );
+               this.markAllReadButton.connect( this, { click: 
'onMarkAllReadButtonClick' } );
 
                // Initialize
                this.toggleMarkAllReadButton();

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I2f55358c16f78e234ec19154b944a4edebfbe639
Gerrit-PatchSet: 60
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to