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