Merge "Echo API layer"

This commit is contained in:
jenkins-bot 2016-03-04 01:00:55 +00:00 committed by Gerrit Code Review
commit 3fd75c41a4
16 changed files with 735 additions and 673 deletions

View file

@ -117,19 +117,14 @@ $wgResourceModules += array(
'viewmodel/mw.echo.dm.js', 'viewmodel/mw.echo.dm.js',
'viewmodel/mw.echo.dm.List.js', 'viewmodel/mw.echo.dm.List.js',
'viewmodel/mw.echo.dm.SortedList.js', 'viewmodel/mw.echo.dm.SortedList.js',
'viewmodel/handlers/mw.echo.dm.APIHandler.js',
'viewmodel/handlers/mw.echo.dm.LocalAPIHandler.js',
'viewmodel/handlers/mw.echo.dm.ForeignAPIHandler.js',
'viewmodel/handlers/mw.echo.dm.NetworkHandler.js',
'viewmodel/mw.echo.dm.NotificationItem.js', 'viewmodel/mw.echo.dm.NotificationItem.js',
'viewmodel/mw.echo.dm.NotificationGroupItem.js', 'viewmodel/mw.echo.dm.NotificationGroupItem.js',
'viewmodel/mw.echo.dm.NotificationList.js', 'viewmodel/mw.echo.dm.NotificationList.js',
'viewmodel/mw.echo.dm.NotificationsModel.js', 'viewmodel/mw.echo.dm.NotificationsModel.js',
), ),
'dependencies' => array( 'dependencies' => array(
'mediawiki.api', 'oojs',
'mediawiki.ForeignApi', 'ext.echo.api',
'oojs'
), ),
'messages' => array( 'messages' => array(
'echo-api-failure', 'echo-api-failure',
@ -137,6 +132,22 @@ $wgResourceModules += array(
), ),
'targets' => array( 'desktop', 'mobile' ), 'targets' => array( 'desktop', 'mobile' ),
), ),
'ext.echo.api' => $echoResourceTemplate + array(
'scripts' => array(
'api/mw.echo.api.js',
'api/mw.echo.api.EchoApi.js',
'api/mw.echo.api.APIHandler.js',
'api/mw.echo.api.LocalAPIHandler.js',
'api/mw.echo.api.ForeignAPIHandler.js',
'api/mw.echo.api.NetworkHandler.js',
),
'dependencies' => array(
'mediawiki.api',
'mediawiki.ForeignApi',
'oojs'
),
'targets' => array( 'desktop', 'mobile' ),
),
'ext.echo.base' => array( 'ext.echo.base' => array(
// This is a dummy module for backwards compatibility. // This is a dummy module for backwards compatibility.
// Most extensions that require ext.echo.base actually need // Most extensions that require ext.echo.base actually need
@ -154,6 +165,7 @@ $wgResourceModules += array(
'scripts' => array( 'scripts' => array(
'ext.echo.init.js', 'ext.echo.init.js',
), ),
'dependencies' => array( 'ext.echo.api' ),
'targets' => array( 'desktop' ), 'targets' => array( 'desktop' ),
), ),
// Base no-js styles // Base no-js styles

View file

@ -0,0 +1,167 @@
( function ( mw, $ ) {
/**
* Abstract notification API handler
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration object
* @cfg {number} [limit=25] The limit on how many notifications to fetch
* @cfg {string} [userLang=mw.config.get( 'wgUserLanguage' )] User language. Defaults
* to the default user language configuration settings.
*/
mw.echo.api.APIHandler = function MwEchoApiAPIHandler( config ) {
config = config || {};
this.fetchNotificationsPromise = {};
this.apiErrorState = {};
this.limit = config.limit || 25;
this.userLang = config.userLang || mw.config.get( 'wgUserLanguage' );
this.api = null;
// Map the logical type to the type
// that the API recognizes
this.normalizedType = {
message: 'message',
alert: 'alert',
all: 'message|alert'
};
// Parameters that are sent through
// to the 'fetch notification' promise
// per type
this.typeParams = {
message: {},
alert: {},
all: {}
};
};
/* Setup */
OO.initClass( mw.echo.api.APIHandler );
/**
* Fetch notifications from the API.
*
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that resolves with an object containing the
* notification items
*/
mw.echo.api.APIHandler.prototype.fetchNotifications = null;
/**
* Create a new fetchNotifications promise that queries the API and overrides
* the cached promise.
*
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that resolves with an object containing the
* notification items
*/
mw.echo.api.APIHandler.prototype.createNewFetchNotificationPromise = function ( type ) {
var me = this,
params = $.extend( {
action: 'query',
meta: 'notifications',
notsections: this.normalizedType[ type ],
notformat: 'model',
notlimit: this.limit,
notunreadfirst: 1,
notprop: 'index|list|count',
uselang: this.userLang
}, this.getTypeParams( type ) );
this.apiErrorState[ type ] = false;
this.fetchNotificationsPromise[ type ] = this.api.get( params )
.fail( function () {
// Mark API error state
me.apiErrorState[ type ] = true;
} );
};
/**
* Update the seen timestamp
*
* @param {string} [type] Notification type 'message', 'alert' or 'all'.
* @return {jQuery.Promise} A promise that resolves with the seen timestamp
*/
mw.echo.api.APIHandler.prototype.updateSeenTime = null;
/**
* Mark all notifications as read
*
* @param {string|string[]} type Notification type 'message', 'alert' or 'all'.
* @return {jQuery.Promise} A promise that resolves when all notifications
* are marked as read.
*/
mw.echo.api.APIHandler.prototype.markAllRead = null;
/**
* Mark multiple notification items as read using specific IDs
*
* @abstract
* @param {string[]} itemIdArray An array of notification item IDs
* @return {jQuery.Promise} A promise that resolves when all given notifications
* are marked as read.
*/
mw.echo.api.APIHandler.prototype.markItemsRead = null;
/**
* Update the read status of a notification item in the API
*
* @param {string} itemId Item id
* @return {jQuery.Promise} A promise that resolves when the notifications
* are marked as read.
*/
mw.echo.api.APIHandler.prototype.markItemRead = function ( itemId ) {
this.markItemsRead( [ itemId ] );
};
/**
* Query the API for unread count of the notifications in this model
*
* @param {string} type Notification type 'message', 'alert' or 'all'.
* @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched
* and the badge label is updated.
*/
mw.echo.api.APIHandler.prototype.fetchUnreadCount = null;
/**
* Check whether the model has an API error state flagged
*
* @param {string} type Notification type, 'alert', 'message' or 'all'
* @return {boolean} The model is in API error state
*/
mw.echo.api.APIHandler.prototype.isFetchingErrorState = function ( type ) {
return !!this.apiErrorState[ type ];
};
/**
* Return the fetch notifications promise
*
* @param {string} type Notification type, 'alert', 'message' or 'all'
* @return {jQuery.Promise} Promise that is resolved when notifications are
* fetched from the API.
*/
mw.echo.api.APIHandler.prototype.getFetchNotificationPromise = function ( type ) {
if ( !this.fetchNotificationsPromise[ type ] ) {
this.createNewFetchNotificationPromise( type );
}
return this.fetchNotificationsPromise[ type ];
};
/**
* Get the extra parameters for fetching notifications for a given
* notification type.
*
* @param {string} type Notification type
* @return {Object} Extra API parameters for fetch notifications
*/
mw.echo.api.APIHandler.prototype.getTypeParams = function ( type ) {
return this.typeParams[ type ];
};
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,157 @@
( function ( mw ) {
/**
* A class defining Echo API instructions and network operations
*
* @constructor
*/
mw.echo.api.EchoApi = function MwEchoApiEchoApi() {
this.network = new mw.echo.api.NetworkHandler();
this.fetchingPromise = null;
};
OO.initClass( mw.echo.api.EchoApi );
/**
* Fetch notifications from the server based on type
* @param {string} types An array of notification types to fetch: 'alert', 'message', 'all'
* @param {string} [source="local"] The source from which to fetch the notifications
* @param {boolean} [isForced] Force a refresh on the fetch notifications promise
* @return {[type]} Promise that is resolved with all notifications for the
* requested types.
*/
mw.echo.api.EchoApi.prototype.fetchNotifications = function ( type, source, isForced ) {
var api = this;
source = source || 'local';
return this.network.getApiHandler( source ).fetchNotifications( type, isForced )
.then( function ( result ) {
var id, s,
sources = {},
rawData = OO.getProp( result.query, 'notifications' ),
sourceDefinitions = rawData.sources;
for ( source in sourceDefinitions ) {
api.network.addApiHandler( source, sourceDefinitions[ source ], true );
}
for ( id in rawData.list ) {
if ( rawData.list[ id ].type === 'external' ) {
// Define sources
sources = {};
for ( s = 0; s < rawData.list[ id ].sources.length; s++ ) {
sources[ rawData.list[ id ].sources[ s ] ] = sourceDefinitions[ rawData.list[ id ].sources[ s ] ].title;
}
rawData.list[ id ].sources = sources;
}
}
return rawData;
} );
};
/**
* Fetch notifications from several sources
*
* @param {string[]} sourceArray An array of sources to fetch from the group
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that resolves with an array of promises for
* fetchNotifications per each given source in the source array.
*/
mw.echo.api.EchoApi.prototype.fetchNotificationGroups = function ( sourceArray, type ) {
var i, promise,
promises = [];
for ( i = 0; i < sourceArray.length; i++ ) {
promise = this.network.getApiHandler( sourceArray[ i ] ).fetchNotifications( type );
promises.push( promise );
}
return mw.echo.api.NetworkHandler.static.waitForAllPromises( promises );
};
/**
* Mark items as read in the API.
*
* @param {string[]} itemIds An array of item IDs to mark as read
* @param {string} source The source that these items belong to
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that is resolved when the operation
* is complete, with the number of unread notifications still remaining
* for that type in the given source
*/
mw.echo.api.EchoApi.prototype.markItemsRead = function ( itemIds, source, type ) {
return this.network.getApiHandler( source ).markItemsRead( itemIds, type );
};
/**
* Mark all notifications for a given type as read in the given source.
*
* @param {[type]} source Notifications source
* @param {[type]} type Notifications type
* @return {jQuery.Promise} A promise that is resolved when the operation
* is complete, with the number of unread notifications still remaining
* for that type in the given source
*/
mw.echo.api.EchoApi.prototype.markAllRead = function ( source, type ) {
// FIXME: This specific method sends an operation
// to the API that marks all notifications of the given type as read regardless
// of whether they were actually seen by the user.
// We should consider removing the use of this method and, instead,
// using strictly the 'markItemsRead' by giving the API only the
// notifications that are available to the user.
return this.network.getApiHandler( source ).markAllRead( type );
};
/**
* Fetch the number of unread notifications for the given type in the given
* source.
*
* @param {string} source Notifications source
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that is resolved with the number of
* unread notifications for the given type and source.
*/
mw.echo.api.EchoApi.prototype.fetchUnreadCount = function ( source, type ) {
return this.network.getApiHandler( source ).fetchUnreadCount( type );
};
/**
* Update the seenTime property for the given type and source.
*
* @param {string} source Notification source
* @param {string} type Notification type
* @return {jQuery.Promise} A promise that is resolved when the operation is complete.
*/
mw.echo.api.EchoApi.prototype.updateSeenTime = function ( source, type ) {
return this.network.getApiHandler( source ).updateSeenTime( type );
};
/**
* Check whether the API promise for fetch notification is in an error
* state for the given source and notification type.
*
* @param {string} source Notification source.
* @param {string} type Notification type
* @return {Boolean} The API response for fetching notification has
* resolved in an error state, or is rejected.
*/
mw.echo.api.EchoApi.prototype.isFetchingErrorState = function ( source, type ) {
return this.network.getApiHandler( source ).isFetchingErrorState( type );
};
/**
* Get the fetch notifications promise active for the current source and type.
*
* @param {string} source Notification source.
* @param {string} type Notification type
* @return {jQuery.Promise} Promise that is resolved when notifications are
* fetched from the API.
*/
mw.echo.api.EchoApi.prototype.getFetchNotificationPromise = function ( source, type ) {
return this.network.getApiHandler( source ).getFetchNotificationPromise( type );
};
} )( mediaWiki );

View file

@ -0,0 +1,41 @@
( function ( mw, $ ) {
/**
* Foreign notification API handler
*
* @class
* @extends mw.echo.api.LocalAPIHandler
*
* @constructor
* @param {string} apiUrl A url for the access point of the
* foreign API.
* @param {Object} [config] Configuration object
*/
mw.echo.api.ForeignAPIHandler = function MwEchoApiForeignAPIHandler( apiUrl, config ) {
config = config || {};
// Parent constructor
mw.echo.api.ForeignAPIHandler.parent.call( this, config );
// Add 'noforn' setting to foreign APIs
$.extend( true, this.typeParams, {
message: {
notnoforn: 1,
notfilter: '!read'
},
alert: {
notnoforn: 1,
notfilter: '!read'
},
all: {
notnoforn: 1,
notfilter: '!read'
}
} );
this.api = new mw.ForeignApi( apiUrl );
};
/* Setup */
OO.inheritClass( mw.echo.api.ForeignAPIHandler, mw.echo.api.LocalAPIHandler );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,97 @@
( function ( mw ) {
/**
* Notification API handler
*
* @class
* @extends mw.echo.dm.APIHandler
*
* @constructor
* @param {Object} [config] Configuration object
*/
mw.echo.api.LocalAPIHandler = function MwEchoApiLocalAPIHandler( config ) {
config = config || {};
// Parent constructor
mw.echo.api.LocalAPIHandler.parent.call( this, config );
this.api = new mw.Api( { ajax: { cache: false } } );
};
/* Setup */
OO.inheritClass( mw.echo.api.LocalAPIHandler, mw.echo.api.APIHandler );
/**
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.fetchNotifications = function ( type, isForced ) {
if ( isForced || this.isFetchingErrorState( type ) ) {
// Force new promise
this.createNewFetchNotificationPromise( type );
}
return this.getFetchNotificationPromise( type );
};
/**
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.updateSeenTime = function ( type ) {
return this.api.postWithToken( 'edit', {
action: 'echomarkseen',
type: this.normalizedType[ type ]
} )
.then( function ( data ) {
return data.query.echomarkseen.timestamp;
} );
};
/**
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.markAllRead = function ( type ) {
var data = {
action: 'echomarkread',
sections: this.normalizedType[ type ]
};
return this.api.postWithToken( 'edit', data )
.then( function ( result ) {
return OO.getProp( result.query, 'echomarkread', type, 'rawcount' ) || 0;
} );
};
/**
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.markItemsRead = function ( itemIdArray ) {
var data = {
action: 'echomarkread',
list: itemIdArray.join( '|' )
};
return this.api.postWithToken( 'edit', data );
};
/**
* @inheritdoc
*/
mw.echo.api.LocalAPIHandler.prototype.fetchUnreadCount = function ( type ) {
var normalizedType = this.normalizedType[ type ],
apiData = {
action: 'query',
meta: 'notifications',
notsections: normalizedType,
notgroupbysection: 1,
notmessageunreadfirst: 1,
notlimit: this.limit,
notprop: 'count',
uselang: this.userLang
};
return this.api.get( apiData )
.then( function ( result ) {
return OO.getProp( result.query, 'notifications', normalizedType, 'rawcount' ) || 0;
} );
};
} )( mediaWiki );

View file

@ -0,0 +1,101 @@
( function ( mw, $ ) {
/**
* Network handler for echo notifications. Manages multiple APIHandlers
* according to their sources.
*
* @class
*
* @constructor
*/
mw.echo.api.NetworkHandler = function MwEchoApiNetworkHandler() {
this.handlers = {};
// Add initial local handler
this.addApiHandler( 'local', {} );
};
/* Setup */
OO.initClass( mw.echo.api.NetworkHandler );
/* Static methods */
/**
* Wait for all promises to finish either with a resolve or reject and
* return them to the caller once they do.
*
* @param {jQuery.Promise[]} promiseArray An array of promises
* @return {jQuery.Promise} A promise that resolves when all the promises
* finished with some resolution or rejection.
*/
mw.echo.api.NetworkHandler.static.waitForAllPromises = function ( promiseArray ) {
var i,
promises = promiseArray.slice( 0 ),
counter = 0,
deferred = $.Deferred(),
countPromises = function () {
counter++;
if ( counter === promises.length ) {
deferred.resolve( promises );
}
};
if ( !promiseArray.length ) {
deferred.resolve();
}
for ( i = 0; i < promises.length; i++ ) {
promises[ i ].always( countPromises );
}
return deferred.promise();
};
/* Methods */
/**
* Get the API handler that matches the symbolic name
*
* @param {string} name Symbolic name of the API handler
* @return {mw.echo.dm.APIHandler|undefined} API handler, if exists
*/
mw.echo.api.NetworkHandler.prototype.getApiHandler = function ( name ) {
return this.handlers[ name ];
};
/**
* Add an API handler
*
* @param {string} name Symbolic name
* @param {Object} config Configuration details
* @param {boolean} isForeign Is a foreign API
* @throws {Error} If no URL was given for a foreign API
*/
mw.echo.api.NetworkHandler.prototype.addApiHandler = function ( name, config, isForeign ) {
// This must be here so that it short-circuits the object construction below
if ( this.handlers[ name ] ) {
return;
}
if ( isForeign ) {
if ( !config.url ) {
throw new Error( 'Foreign APIs must have a valid url.' );
}
this.addCustomApiHandler( name, new mw.echo.api.ForeignAPIHandler( config.url, config ) );
} else {
this.addCustomApiHandler( name, new mw.echo.api.LocalAPIHandler( config ) );
}
};
/**
* Add a custom 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
*/
mw.echo.api.NetworkHandler.prototype.addCustomApiHandler = function ( name, handler ) {
if ( !this.handlers[ name ] ) {
this.handlers[ name ] = handler;
}
};
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,4 @@
( function ( mw ) {
mw.echo = mw.echo || {};
mw.echo.api = {};
} )( mediaWiki );

View file

@ -3,23 +3,9 @@
mw.echo = mw.echo || {}; mw.echo = mw.echo || {};
mw.echo.apiCallParams = {
action: 'query',
meta: 'notifications',
// We have to send the API 'groupbysection' otherwise
// the 'messageunreadfirst' doesn't do anything.
// TODO: Fix the API.
notgroupbysection: 1,
notmessageunreadfirst: 1,
notformat: 'model',
notlimit: 25,
notprop: 'index|list|count',
uselang: mw.config.get( 'wgUserLanguage' )
};
// Activate ooui // Activate ooui
$( document ).ready( function () { $( document ).ready( function () {
var apiRequest, myWidget, var myWidget, echoApi,
$existingAlertLink = $( '#pt-notifications-alert a' ), $existingAlertLink = $( '#pt-notifications-alert a' ),
$existingMessageLink = $( '#pt-notifications-message a' ), $existingMessageLink = $( '#pt-notifications-message a' ),
numAlerts = $existingAlertLink.text(), numAlerts = $existingAlertLink.text(),
@ -45,7 +31,8 @@
$( this ).addClass( 'mw-echo-notifications-badge-dimmed' ); $( this ).addClass( 'mw-echo-notifications-badge-dimmed' );
// Fire the notification API requests // Fire the notification API requests
apiRequest = new mw.Api( { ajax: { cache: false } } ).get( $.extend( { notsections: myType }, mw.echo.apiCallParams ) ) echoApi = new mw.echo.api.EchoApi();
echoApi.fetchNotifications( myType )
.then( function ( data ) { .then( function ( data ) {
mw.track( 'timing.MediaWiki.echo.overlay.api', mw.now() - time ); mw.track( 'timing.MediaWiki.echo.overlay.api', mw.now() - time );
return data; return data;
@ -61,11 +48,7 @@
// Load message button and popup if messages exist // Load message button and popup if messages exist
if ( $existingMessageLink.length ) { if ( $existingMessageLink.length ) {
messageNotificationsModel = new mw.echo.dm.NotificationsModel( messageNotificationsModel = new mw.echo.dm.NotificationsModel(
// Create a network handler echoApi,
new mw.echo.dm.NetworkHandler( {
type: 'message',
baseParams: mw.echo.apiCallParams
} ),
{ {
type: 'message' type: 'message'
} }
@ -93,11 +76,7 @@
} }
// Load alerts popup and button // Load alerts popup and button
alertNotificationsModel = new mw.echo.dm.NotificationsModel( alertNotificationsModel = new mw.echo.dm.NotificationsModel(
// Create a network handler echoApi,
new mw.echo.dm.NetworkHandler( {
type: 'alert',
baseParams: mw.echo.apiCallParams
} ),
{ {
type: 'alert' type: 'alert'
} }
@ -121,11 +100,8 @@
// HACK: Now that the module loaded, show the popup // HACK: Now that the module loaded, show the popup
myWidget = myType === 'alert' ? mw.echo.ui.alertWidget : mw.echo.ui.messageWidget; myWidget = myType === 'alert' ? mw.echo.ui.alertWidget : mw.echo.ui.messageWidget;
myWidget.populateNotifications( apiRequest ).then( function () { myWidget.once( 'finishLoading', function () {
// Log timing after notifications are shown // Log timing after notifications are shown
// FIXME: The notifications might not be shown yet if the API
// request finished before the UI loads, in which case it will
// only be shown after the toggle( true ) call below.
mw.track( 'timing.MediaWiki.echo.overlay', mw.now() - time ); mw.track( 'timing.MediaWiki.echo.overlay', mw.now() - time );
} ); } );
myWidget.popup.toggle( true ); myWidget.popup.toggle( true );

View file

@ -179,12 +179,22 @@
* All notifications were marked as read * All notifications were marked as read
*/ */
/**
* @event finishLoading
* Notifications have successfully finished being processed and are fully loaded
*/
/* Methods */ /* Methods */
/** /**
* Respond to badge button click * Respond to badge button click
*/ */
mw.echo.ui.NotificationBadgeWidget.prototype.onBadgeButtonClick = function () { mw.echo.ui.NotificationBadgeWidget.prototype.onBadgeButtonClick = function () {
if ( !this.popup.isVisible() ) {
// Force a new API request for notifications
this.notificationsModel.fetchNotifications( true );
}
this.popup.toggle(); this.popup.toggle();
}; };
@ -248,51 +258,17 @@
this.notificationsModel.markAllRead(); this.notificationsModel.markAllRead();
}; };
/**
* Populate notifications from the API.
*
* @param {jQuery.Promise} [fetchingApiRequest] An existing promise for fetching
* notifications from the API. This allows us to start fetching notifications
* externally.
* @return {jQuery.Promise} Promise that is resolved when the notifications populate
*/
mw.echo.ui.NotificationBadgeWidget.prototype.populateNotifications = function ( fetchingApiRequest ) {
var widget = this;
// The model retrieves the ongoing promise or returns the existing one that it
// has. When the promise is completed successfuly, it nullifies itself so we can
// request for it to be rebuilt and the request to the API resent.
// However, in the case of an API failure, the promise does not nullify itself.
// In that case we also want the model to rebuild the request, so in this condition
// we must check both cases.
if ( !this.notificationsModel.isFetchingNotifications() || this.notificationsModel.isFetchingErrorState() ) {
this.pushPending();
this.markAllReadButton.toggle( false );
return this.notificationsModel.fetchNotifications( fetchingApiRequest )
.then( function ( idArray ) {
// Clip again
widget.popup.clip();
// Log impressions
mw.echo.logger.logNotificationImpressions( this.type, idArray, mw.echo.Logger.static.context.popup );
} )
.always( function () {
// Pop pending
widget.popPending();
// Nullify the promise; let the user fetch again
widget.fetchNotificationsPromise = null;
} );
} else {
return this.notificationsModel.getFetchNotificationPromise();
}
};
/** /**
* Extend the response to button click so we can also update the notification list. * Extend the response to button click so we can also update the notification list.
* @fires finishLoading
*/ */
mw.echo.ui.NotificationBadgeWidget.prototype.onPopupToggle = function ( isVisible ) { mw.echo.ui.NotificationBadgeWidget.prototype.onPopupToggle = function ( isVisible ) {
var widget = this; var widget = this;
if ( this.promiseRunning ) {
return;
}
if ( !isVisible ) { if ( !isVisible ) {
// If the popup is closing, remove "initiallyUnseen" and leave // If the popup is closing, remove "initiallyUnseen" and leave
this.notificationsWidget.resetNotificationItems(); this.notificationsWidget.resetNotificationItems();
@ -316,14 +292,20 @@
this.popup.$clippable.css( 'height', '1px' ); this.popup.$clippable.css( 'height', '1px' );
this.popup.clip(); this.popup.clip();
} }
this.pushPending();
this.markAllReadButton.toggle( false );
this.promiseRunning = true;
// Always populate on popup open. The model and widget should handle // Always populate on popup open. The model and widget should handle
// the case where the promise is already underway. // the case where the promise is already underway.
this.populateNotifications() this.notificationsModel.fetchNotifications()
.then( function () { .then( function () {
var i, var i,
items = widget.notificationsWidget.getItems(); items = widget.notificationsWidget.getItems();
if ( widget.popup.isVisible() ) { if ( widget.popup.isVisible() ) {
widget.popup.clip();
// Update seen time // Update seen time
widget.notificationsModel.updateSeenTime(); widget.notificationsModel.updateSeenTime();
// Mark notifications as 'read' if markReadWhenSeen is set to true // Mark notifications as 'read' if markReadWhenSeen is set to true
@ -332,6 +314,8 @@
} }
// Log impressions // Log impressions
// TODO: Only log the impressions of notifications that
// are actually visible
for ( i = 0; i < items.length; i++ ) { for ( i = 0; i < items.length; i++ ) {
if ( items[ i ].getModel ) { if ( items[ i ].getModel ) {
mw.echo.logger.logInteraction( mw.echo.logger.logInteraction(
@ -343,6 +327,13 @@
} }
} }
} }
widget.emit( 'finishLoading' );
} )
.always( function () {
// Pop pending
widget.popPending();
widget.promiseRunning = false;
} ); } );
this.hasRunFirstTime = true; this.hasRunFirstTime = true;
}; };

View file

@ -1,130 +0,0 @@
( function ( mw ) {
/**
* Abstract notification API handler
*
* @abstract
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration object
* @cfg {Object} [baseParams] The base parameters for all API calls
* done by the API handler
* @cfg {number} [limit=25] The limit on how many notifications to fetch
* @cfg {string} [type='alert'] Notification type
* @cfg {string} [userLang='en'] User language
*/
mw.echo.dm.APIHandler = function MwEchoDmAPIHandler( config ) {
config = config || {};
// Mixin constructor
OO.EventEmitter.call( this );
this.fetchNotificationsPromise = null;
this.apiErrorState = false;
this.type = config.type || 'alert';
this.limit = config.limit || 25;
this.userLang = config.userLang || 'en';
this.baseParams = config.baseParams || {};
this.api = null;
};
/* Setup */
OO.initClass( mw.echo.dm.APIHandler );
OO.mixinClass( mw.echo.dm.APIHandler, OO.EventEmitter );
/**
* Fetch notifications from the API.
*
* @param {jQuery.Promise} [apiPromise] An existing promise querying the API for notifications.
* This allows us to send an API request foreign to the DM and have the model
* handle the operation as if it asked for the request itself, updating all that
* needs to be updated and emitting all proper events.
* @return {jQuery.Promise} A promise that resolves with an object containing the
* notification items
*/
mw.echo.dm.APIHandler.prototype.fetchNotifications = null;
/**
* Update the seen timestamp
*
* @param {string|string[]} [type] Notification type 'message', 'alert' or
* an array of both.
* @return {jQuery.Promise} A promise that resolves with the seen timestamp
*/
mw.echo.dm.APIHandler.prototype.updateSeenTime = null;
/**
* Mark all notifications as read
*
* @return {jQuery.Promise} A promise that resolves when all notifications
* are marked as read.
*/
mw.echo.dm.APIHandler.prototype.markAllRead = null;
/**
* Mark multiple notification items as read using specific IDs
*
* @abstract
* @param {string[]} itemIdArray An array of notification item IDs
* @return {jQuery.Promise} A promise that resolves when all given notifications
* are marked as read.
*/
mw.echo.dm.APIHandler.prototype.markMultipleItemsRead = null;
/**
* Update the read status of a notification item in the API
*
* @param {string} itemId Item id
* @return {jQuery.Promise} A promise that resolves when the notifications
* are marked as read.
*/
mw.echo.dm.APIHandler.prototype.markItemRead = null;
/**
* Query the API for unread count of the notifications in this model
*
* @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched
* and the badge label is updated.
*/
mw.echo.dm.APIHandler.prototype.fetchUnreadCount = null;
/**
* Check whether the model is fetching notifications from the API
*
* @return {boolean} The model is in the process of fetching from the API
*/
mw.echo.dm.APIHandler.prototype.isFetchingNotifications = function () {
return !!this.fetchNotificationsPromise;
};
/**
* Check whether the model has an API error state flagged
*
* @return {boolean} The model is in API error state
*/
mw.echo.dm.APIHandler.prototype.isFetchingErrorState = function () {
return !!this.apiErrorState;
};
/**
* Return the fetch notifications promise
* @return {jQuery.Promise} Promise that is resolved when notifications are
* fetched from the API.
*/
mw.echo.dm.APIHandler.prototype.getFetchNotificationPromise = function () {
return this.fetchNotificationsPromise;
};
/**
* Get the base params associated with this API handler
*
* @return {Object} Base API params
*/
mw.echo.dm.APIHandler.prototype.getBaseParams = function () {
return this.baseParams;
};
} )( mediaWiki );

View file

@ -1,25 +0,0 @@
( function ( mw ) {
/**
* Foreign notification API handler
*
* @class
* @extends mw.echo.dm.LocalAPIHandler
*
* @constructor
* @param {string} apiUrl A url for the access point of the
* foreign API.
* @param {Object} [config] Configuration object
*/
mw.echo.dm.ForeignAPIHandler = function MwEchoDmForeignAPIHandler( apiUrl, config ) {
config = config || {};
// Parent constructor
mw.echo.dm.ForeignAPIHandler.parent.call( this, config );
this.api = new mw.ForeignApi( apiUrl );
};
/* Setup */
OO.inheritClass( mw.echo.dm.ForeignAPIHandler, mw.echo.dm.LocalAPIHandler );
} )( mediaWiki );

View file

@ -1,133 +0,0 @@
( function ( mw, $ ) {
/**
* Notification API handler
*
* @class
* @extends mw.echo.dm.APIHandler
*
* @constructor
* @param {Object} [config] Configuration object
*/
mw.echo.dm.LocalAPIHandler = function MwEchoDmLocalAPIHandler( config ) {
config = config || {};
// Parent constructor
mw.echo.dm.LocalAPIHandler.parent.call( this, config );
this.api = new mw.Api( { ajax: { cache: false } } );
};
/* Setup */
OO.inheritClass( mw.echo.dm.LocalAPIHandler, mw.echo.dm.APIHandler );
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.fetchNotifications = function ( apiPromise ) {
var helper = this,
params = $.extend( { notsections: this.type }, this.getBaseParams() );
if ( !this.fetchNotificationsPromise || this.isFetchingErrorState() ) {
this.apiErrorState = false;
this.fetchNotificationsPromise = ( apiPromise || this.api.get( params ) )
.fail( function () {
// Mark API error state
helper.apiErrorState = true;
} )
.always( function () {
helper.fetchNotificationsPromise = null;
} );
}
return this.fetchNotificationsPromise;
};
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.updateSeenTime = function ( type ) {
type = type || this.type;
return this.api.postWithToken( 'edit', {
action: 'echomarkseen',
// TODO: The API should also work with piped types like
// getting notification lists
type: $.isArray( type ) ? 'all' : type
} )
.then( function ( data ) {
return data.query.echomarkseen.timestamp;
} );
};
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.markAllRead = function () {
var model = this,
data = {
action: 'echomarkread',
uselang: this.userLang,
sections: $.isArray( this.type ) ? this.type.join( '|' ) : this.type
};
return this.api.postWithToken( 'edit', data )
.then( function ( result ) {
return OO.getProp( result.query, 'echomarkread', model.type, 'rawcount' ) || 0;
} );
};
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.markMultipleItemsRead = function ( itemIdArray ) {
var model = this,
data = {
action: 'echomarkread',
uselang: this.userLang,
list: itemIdArray.join( '|' )
};
return this.api.postWithToken( 'edit', data )
.then( function ( result ) {
return OO.getProp( result.query, 'echomarkread', model.type, 'rawcount' ) || 0;
} );
};
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.markItemRead = function ( itemId ) {
var model = this,
data = {
action: 'echomarkread',
uselang: this.userLang,
list: itemId
};
return this.api.postWithToken( 'edit', data )
.then( function ( result ) {
return OO.getProp( result.query, 'echomarkread', model.type, 'rawcount' ) || 0;
} );
};
/**
* @inheritdoc
*/
mw.echo.dm.LocalAPIHandler.prototype.fetchUnreadCount = function () {
var apiData = {
action: 'query',
meta: 'notifications',
notsections: $.isArray( this.type ) ? this.type.join( '|' ) : this.type,
notmessageunreadfirst: 1,
notlimit: this.limit,
notprop: 'index|count',
uselang: this.userLang
};
return this.api.get( apiData )
.then( function ( result ) {
return OO.getProp( result.query, 'notifications', 'rawcount' ) || 0;
} );
};
} )( mediaWiki, jQuery );

View file

@ -1,143 +0,0 @@
( function ( mw, $ ) {
/**
* Network handler for echo notifications. Manages multiple APIHandlers
* according to their sources.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration object
* @cfg {string} [type="alert"] Notification type
* @cfg {Object} [baseParams] The base params to send to the
* APIs with every fetch notifications process.
*/
mw.echo.dm.NetworkHandler = function MwEchoDmNetworkHandler( config ) {
config = config || {};
// Mixin constructor
OO.EventEmitter.call( this );
this.type = config.type || 'alert';
this.baseParams = config.baseParams || {};
this.handlers = {};
// Add initial local handler
this.addApiHandler( 'local', {} );
};
/* Setup */
OO.initClass( mw.echo.dm.NetworkHandler );
OO.mixinClass( mw.echo.dm.NetworkHandler, OO.EventEmitter );
/* Static methods */
/**
* Wait for all promises to finish either with a resolve or reject and
* return them to the caller once they do.
*
* @param {jQuery.Promise[]} promiseArray An array of promises
* @return {jQuery.Promise} A promise that resolves when all the promises
* finished with some resolution or rejection.
*/
mw.echo.dm.NetworkHandler.static.waitForAllPromises = function ( promiseArray ) {
var i,
promises = promiseArray.slice( 0 ),
counter = 0,
deferred = $.Deferred(),
countPromises = function () {
counter++;
if ( counter === promises.length ) {
deferred.resolve( promises );
}
};
if ( !promiseArray.length ) {
deferred.resolve();
}
for ( i = 0; i < promises.length; i++ ) {
promises[ i ].always( countPromises );
}
return deferred.promise();
};
/* Methods */
/**
* Fetch notifications from several sources
*
* @param {string[]} sourceArray Sources
* @return {jQuery.Promise} A promise that resolves with an array of promises for
* fetchNotifications per each given source in the source array.
*/
mw.echo.dm.NetworkHandler.prototype.fetchNotificationGroups = function ( sourceArray ) {
var i, promise,
promises = [];
for ( i = 0; i < sourceArray.length; i++ ) {
promise = this.getApiHandler( sourceArray[ i ] ).fetchNotifications();
promises.push( promise );
}
return this.constructor.static.waitForAllPromises( promises );
};
/**
* Get the API handler that matches the symbolic name
*
* @param {string} name Symbolic name of the API handler
* @return {mw.echo.dm.APIHandler|undefined} API handler, if exists
*/
mw.echo.dm.NetworkHandler.prototype.getApiHandler = function ( name ) {
return this.handlers[ name ];
};
/**
* Add an API handler
*
* @param {string} name Symbolic name
* @param {Object} config Configuration details
* @param {boolean} isForeign Is a foreign API
* @throws {Error} If no URL was given for a foreign API
*/
mw.echo.dm.NetworkHandler.prototype.addApiHandler = function ( name, config, isForeign ) {
var apiConfig;
if ( !this.handlers[ name ] ) {
apiConfig = $.extend( true, {}, { baseParams: this.baseParams, type: this.getType() }, config );
if ( isForeign ) {
if ( !config.url ) {
throw new Error( 'Foreign APIs must have a valid url.' );
}
this.addCustomApiHandler( name, new mw.echo.dm.ForeignAPIHandler( config.url, apiConfig ) );
} else {
this.addCustomApiHandler( name, new mw.echo.dm.LocalAPIHandler( apiConfig ) );
}
}
};
/**
* Add a custom API handler by passing in an instance of an mw.echo.dm.APIHandler subclass directly.
*
* @param {string} name Symbolic name
* @param {mw.echo.dm.APIHandler} handler Handler object
*/
mw.echo.dm.NetworkHandler.prototype.addCustomApiHandler = function ( name, handler ) {
if ( !this.handlers[ name ] ) {
this.handlers[ name ] = handler;
}
};
/**
* Get the type of notifications this network handler is associated with
*
* @return {string} Notification type
*/
mw.echo.dm.NetworkHandler.prototype.getType = function () {
return this.type;
};
} )( mediaWiki, jQuery );

View file

@ -7,7 +7,7 @@
* @mixins mw.echo.dm.SortedList * @mixins mw.echo.dm.SortedList
* *
* @constructor * @constructor
* @param {mw.echo.dm.NetworkHandler} networkHandler The network handler * @param {mw.echo.api.EchoApi} api Echo API
* @param {Object[]} sources An array of objects defining the sources * @param {Object[]} sources An array of objects defining the sources
* of its item's sub-items. * of its item's sub-items.
* @param {number} id Notification id, * @param {number} id Notification id,
@ -17,7 +17,7 @@
* @cfg {number} [count=0] The number of items this group contains. This is used for both the * @cfg {number} [count=0] The number of items this group contains. This is used for both the
* 'expand' label and also to potentially update the badge counters for local bundles. * 'expand' label and also to potentially update the badge counters for local bundles.
*/ */
mw.echo.dm.NotificationGroupItem = function mwEchoDmNotificationGroupItem( networkHandler, sources, id, config ) { mw.echo.dm.NotificationGroupItem = function mwEchoDmNotificationGroupItem( api, sources, id, config ) {
var source, item, var source, item,
items = []; items = [];
@ -49,18 +49,15 @@
this.removeReadNotifications = !!config.removeReadNotifications; this.removeReadNotifications = !!config.removeReadNotifications;
this.sources = sources; this.sources = sources;
this.networkHandler = networkHandler; this.api = api;
this.notifModels = {}; this.notifModels = {};
this.anticipatedCount = config.count || 0; this.anticipatedCount = config.count || 0;
// Create notification models for each source // Create notification models for each source
for ( source in this.sources ) { for ( source in this.sources ) {
// Add foreign API handler
this.networkHandler.addApiHandler( source, { url: this.sources[ source ].url, baseParams: { notnoforn: 1, notfilter: '!read' } }, true );
// Create a notifications model // Create a notifications model
item = new mw.echo.dm.NotificationsModel( item = new mw.echo.dm.NotificationsModel(
this.networkHandler, this.api,
{ {
type: this.getType(), type: this.getType(),
source: source, source: source,
@ -124,19 +121,19 @@
fetchPromises = [], fetchPromises = [],
sourceKeys = Object.keys( this.sources ); sourceKeys = Object.keys( this.sources );
return this.networkHandler.fetchNotificationGroups( sourceKeys ) return this.api.fetchNotificationGroups( sourceKeys, this.getType() )
.then( function ( promises ) { .then( function () {
var i; var i;
for ( i = 0; i < sourceKeys.length; i++ ) { for ( i = 0; i < sourceKeys.length; i++ ) {
notifModel = model.getItemById( sourceKeys[ i ] ); notifModel = model.getItemById( sourceKeys[ i ] );
if ( notifModel ) { if ( notifModel ) {
fetchPromises.push( notifModel.fetchNotifications( promises[ i ] ) ); fetchPromises.push( notifModel.fetchNotifications() );
} }
} }
// Wait for all fetch processes to finish before we resolve this promise // Wait for all fetch processes to finish before we resolve this promise
return mw.echo.dm.NetworkHandler.static.waitForAllPromises( fetchPromises ); return mw.echo.api.NetworkHandler.static.waitForAllPromises( fetchPromises );
} ) } )
.then( function () { .then( function () {
model.anticipatedCount = null; model.anticipatedCount = null;
@ -180,9 +177,9 @@
.then( ( function ( model ) { .then( ( function ( model ) {
return function ( idArray ) { return function ( idArray ) {
// Mark sub items as read in the UI // Mark sub items as read in the UI
model.markAllRead(); model.markAllRead( model.getSource(), model.getType() );
// Mark all existing items as read in the API // Mark all existing items as read in the API
model.markExistingItemsReadInApi( idArray ); model.markItemsReadInApi( idArray );
}; };
} )( notifModels[ i ] ) ); } )( notifModels[ i ] ) );
} }

View file

@ -6,7 +6,7 @@
* @mixins OO.EventEmitter * @mixins OO.EventEmitter
* *
* @constructor * @constructor
* @param {mw.echo.dm.NetworkHandler} networkHandler Network handler * @param {mw.echo.api.EchoApi} api Network handler
* @param {Object} [config] Configuration object * @param {Object} [config] Configuration object
* @cfg {string} [id] Model id, used to refer to the model specifically. * @cfg {string} [id] Model id, used to refer to the model specifically.
* Falls back to the model's unique source * Falls back to the model's unique source
@ -23,7 +23,7 @@
* means the model will only contain unread notifications. This is useful for * means the model will only contain unread notifications. This is useful for
* cross-wiki bundled notifications. * cross-wiki bundled notifications.
*/ */
mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( networkHandler, config ) { mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( api, config ) {
config = config || {}; config = config || {};
// Mixin constructor // Mixin constructor
@ -43,7 +43,7 @@
this.removeReadNotifications = !!config.removeReadNotifications; this.removeReadNotifications = !!config.removeReadNotifications;
this.foreign = !!config.foreign; this.foreign = !!config.foreign;
this.networkHandler = networkHandler; this.api = api;
this.seenTime = mw.config.get( 'wgEchoSeenTime' ) || {}; this.seenTime = mw.config.get( 'wgEchoSeenTime' ) || {};
@ -186,7 +186,7 @@
// because the API takes a single request to mark all notifications // because the API takes a single request to mark all notifications
// as read, and we don't need to send multiple individual requests. // as read, and we don't need to send multiple individual requests.
if ( !this.markingAllAsRead ) { if ( !this.markingAllAsRead ) {
this.markItemReadInApi( id ); this.markItemsReadInApi( id );
} }
if ( this.removeReadNotifications ) { if ( this.removeReadNotifications ) {
// Remove this notification from the model // Remove this notification from the model
@ -341,7 +341,7 @@
// Only update seenTime in the API locally // Only update seenTime in the API locally
if ( !this.isForeign() ) { if ( !this.isForeign() ) {
promise = this.getApi().updateSeenTime( type ); promise = this.api.updateSeenTime( this.getSource(), type );
} else { } else {
promise = $.Deferred().resolve(); promise = $.Deferred().resolve();
} }
@ -363,8 +363,8 @@
length = items.length; length = items.length;
// Skip if this is an automatic "mark as read" and this model is // Skip if this is an automatic "mark as read" and this model is
// external // foreign
if ( this.external && this.autoMarkReadInProcess ) { if ( this.foreign && this.autoMarkReadInProcess ) {
return $.Deferred().resolve( 0 ).promise(); return $.Deferred().resolve( 0 ).promise();
} }
@ -382,7 +382,7 @@
this.markingAllAsRead = true; this.markingAllAsRead = true;
for ( i = 0, len = items.length; i < len; i++ ) { for ( i = 0, len = items.length; i < len; i++ ) {
// Skip items that are external if we are in automatic 'mark all as read' // Skip items that are foreign if we are in automatic 'mark all as read'
if ( !items[ i ].isForeign() || !this.autoMarkReadInProcess ) { if ( !items[ i ].isForeign() || !this.autoMarkReadInProcess ) {
items[ i ].toggleRead( true ); items[ i ].toggleRead( true );
items[ i ].toggleSeen( true ); items[ i ].toggleSeen( true );
@ -391,7 +391,7 @@
} }
this.markingAllAsRead = false; this.markingAllAsRead = false;
return this.getApi().markAllRead(); return this.api.markAllRead( this.getSource(), this.getType() );
}; };
/** /**
@ -408,40 +408,27 @@
var model = this; var model = this;
this.autoMarkReadInProcess = true; this.autoMarkReadInProcess = true;
this.markAllRead() this.markAllRead( this.getSource(), this.getType() )
.then( function () { .then( function () {
model.autoMarkReadInProcess = false; model.autoMarkReadInProcess = false;
} ); } );
}; };
/** /**
* Update the read status of a notification item in the API * Update the read status of a notification item, or a list of items, in the API
* *
* @param {string} itemId Item id * @param {string|string[]} itemIds Item id or an array of item Ids
* @return {jQuery.Promise} A promise that resolves when the notifications * @return {jQuery.Promise} A promise that resolves when the notifications
* were marked as read. * were marked as read.
*/ */
mw.echo.dm.NotificationsModel.prototype.markItemReadInApi = function ( itemId ) { mw.echo.dm.NotificationsModel.prototype.markItemsReadInApi = function ( itemIds ) {
if ( !this.unreadNotifications.getItemCount() ) { if ( !this.unreadNotifications.getItemCount() ) {
return $.Deferred().resolve( 0 ).promise(); return $.Deferred().resolve( 0 ).promise();
} }
return this.getApi().markItemRead( itemId ); itemIds = $.isArray( itemIds ) ? itemIds : [ itemIds ];
};
/** return this.api.markItemsRead( itemIds, this.getSource(), this.getType() );
* Update the read status in the API only for the existing items in this model.
* If an item id array is given, those items will be updated. Otherwise, all items
* in the model are updated.
*
* @param {string[]} [itemIds] Array of item ids
* @return {jQuery.Promise} A promise that resolves when the notifications
* were marked as read.
*/
mw.echo.dm.NotificationsModel.prototype.markExistingItemsReadInApi = function ( itemIds ) {
itemIds = itemIds || this.getAllItemIds();
return this.getApi().markMultipleItemsRead( itemIds );
}; };
/** /**
@ -464,34 +451,31 @@
/** /**
* Fetch notifications from the API and update the notifications list. * Fetch notifications from the API and update the notifications list.
* *
* @param {jQuery.Promise} An existing promise querying the API for notifications. * @param {boolean} [isForced] Force a renewed fetching promise. If set to false, the
* This allows us to send an API request external to the DM and have the model * model will request the stored/cached fetching promise from the API. A 'true' value
* handle the operation as if it asked for the request itself, updating all that * will force the API to re-request that information from the server and update the
* needs to be updated and emitting all proper events. * notifications.
* @return {jQuery.Promise} A promise that resolves with an array of notification IDs * @return {jQuery.Promise} A promise that resolves with an array of notification IDs
*/ */
mw.echo.dm.NotificationsModel.prototype.fetchNotifications = function ( apiPromise ) { mw.echo.dm.NotificationsModel.prototype.fetchNotifications = function ( isForced ) {
var model = this; var model = this;
// Rebuild the notifications promise either when it is null or when // Rebuild the notifications promise either when it is null or when
// it exists in a failed state // it exists in a failed state
return ( apiPromise || this.getApi().fetchNotifications() ) return this.api.fetchNotifications( this.getType(), this.getSource(), !!isForced )
.then( .then(
// Success // Success
function ( result ) { function ( data ) {
var notifData, id, t, tlen, s, var notifData, id, s,
notificationModel, types, content, notificationModel, content,
newNotifData = {}, newNotifData = {},
sources = {}, sources = {},
optionItems = [], optionItems = [],
idArray = [], idArray = [],
data = OO.getProp( result.query, 'notifications', model.type ) || { index: [] }, sourceDefinitions = data.sources || {};
sourceDefinitions = OO.getProp( result.query, 'notifications', 'sources' ) || {};
types = $.isArray( model.type ) ? model.type : [ model.type ]; data = data || {};
for ( t = 0, tlen = types.length; t < tlen; t++ ) {
data = OO.getProp( result.query, 'notifications', types[ t ] ) || { list: [] };
for ( id in data.list ) { for ( id in data.list ) {
notifData = data.list[ id ]; notifData = data.list[ id ];
content = notifData[ '*' ] || {}; content = notifData[ '*' ] || {};
@ -525,14 +509,13 @@
if ( notifData.type === 'foreign' ) { if ( notifData.type === 'foreign' ) {
// Define sources // Define sources
sources = {};
for ( s = 0; s < notifData.sources.length; s++ ) { for ( s = 0; s < notifData.sources.length; s++ ) {
sources[ notifData.sources[ s ] ] = sourceDefinitions[ notifData.sources[ s ] ]; sources[ notifData.sources[ s ] ] = sourceDefinitions[ notifData.sources[ s ] ];
} }
// Create model // Create model
notificationModel = new mw.echo.dm.NotificationGroupItem( notificationModel = new mw.echo.dm.NotificationGroupItem(
model.networkHandler, model.api,
sources, sources,
id, id,
$.extend( true, {}, newNotifData, { $.extend( true, {}, newNotifData, {
@ -558,7 +541,6 @@
idArray.push( notifData.id ); idArray.push( notifData.id );
optionItems.push( notificationModel ); optionItems.push( notificationModel );
} }
}
// Add to the model // Add to the model
model.addItems( optionItems, 0 ); model.addItems( optionItems, 0 );
@ -641,19 +623,9 @@
* Query the API for unread count of the notifications in this model * Query the API for unread count of the notifications in this model
* *
* @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched * @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched
* and the badge label is updated.
*/ */
mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi = function () { mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi = function () {
return this.getApi().fetchUnreadCount(); return this.api.fetchUnreadCount( this.getSource(), this.getType() );
};
/**
* Check whether the model is fetching notifications from the API
*
* @return {boolean} The model is in the process of fetching from the API
*/
mw.echo.dm.NotificationsModel.prototype.isFetchingNotifications = function () {
return this.getApi().isFetchingNotifications();
}; };
/** /**
@ -662,7 +634,7 @@
* @return {boolean} The model is in api error state * @return {boolean} The model is in api error state
*/ */
mw.echo.dm.NotificationsModel.prototype.isFetchingErrorState = function () { mw.echo.dm.NotificationsModel.prototype.isFetchingErrorState = function () {
return this.getApi().isFetchingErrorState(); return this.api.isFetchingErrorState( this.getSource(), this.getType() );
}; };
/** /**
@ -671,7 +643,7 @@
* fetched from the API. * fetched from the API.
*/ */
mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise = function () { mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise = function () {
return this.getApi().getFetchNotificationPromise(); return this.api.getFetchNotificationPromise( this.getSource(), this.getType() );
}; };
/** /**
@ -687,24 +659,6 @@
return items[ 0 ] && items[ 0 ].getTimestamp(); return items[ 0 ] && items[ 0 ].getTimestamp();
}; };
/**
* Get the API handler associated with this model's source
*
* @return {mw.echo.dm.APIHandler} API handler
*/
mw.echo.dm.NotificationsModel.prototype.getApi = function () {
return this.networkHandler.getApiHandler( this.source );
};
/**
* Get the network handler
*
* @return {mw.echo.dm.NetworkHandler} Network handler
*/
mw.echo.dm.NotificationsModel.prototype.getNetworkHandler = function () {
return this.networkHandler;
};
/** /**
* Get the source this model is associated with * Get the source this model is associated with
* *

View file

@ -22,12 +22,17 @@
TestApiHandler.parent.call( this ); TestApiHandler.parent.call( this );
} }
/* Setup */ /* Setup */
OO.inheritClass( TestApiHandler, mw.echo.dm.APIHandler ); OO.inheritClass( TestApiHandler, mw.echo.api.APIHandler );
// Override api call // Override api call
TestApiHandler.prototype.markItemRead = function () { TestApiHandler.prototype.markItemsRead = function () {
return $.Deferred().resolve( 0 ); return $.Deferred().resolve( 0 );
}; };
// Create an Echo API instance
var echoApi = new mw.echo.api.EchoApi();
// HACK: Reach into the EchoAPI to create a test handler
echoApi.network.addCustomApiHandler( 'test', new TestApiHandler() );
QUnit.test( 'Adding notifications', function ( assert ) { QUnit.test( 'Adding notifications', function ( assert ) {
var initialItems = [ var initialItems = [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ), new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
@ -88,15 +93,13 @@
cases.forEach( function ( test ) { cases.forEach( function ( test ) {
var r, runCase, runItem, var r, runCase, runItem,
networkHandler = new mw.echo.dm.NetworkHandler(), model = new mw.echo.dm.NotificationsModel( echoApi, {
model = new mw.echo.dm.NotificationsModel( networkHandler, {
type: 'alert', type: 'alert',
source: 'test', source: 'test',
limit: 25, limit: 25,
userLang: 'en' userLang: 'en'
} ); } );
networkHandler.addCustomApiHandler( 'test', new TestApiHandler() );
model.addItems( test.items ); model.addItems( test.items );
if ( test.add ) { if ( test.add ) {
@ -115,8 +118,7 @@
} ); } );
QUnit.test( 'Deleting notifications', 2, function ( assert ) { QUnit.test( 'Deleting notifications', 2, function ( assert ) {
var networkHandler = new mw.echo.dm.NetworkHandler(), var model = new mw.echo.dm.NotificationsModel( echoApi, {
model = new mw.echo.dm.NotificationsModel( networkHandler, {
type: 'alert', type: 'alert',
source: 'test', source: 'test',
limit: 25, limit: 25,
@ -135,7 +137,6 @@
new mw.echo.dm.NotificationItem( 10, { content: '10', timestamp: '20150828172900' } ) new mw.echo.dm.NotificationItem( 10, { content: '10', timestamp: '20150828172900' } )
]; ];
networkHandler.addCustomApiHandler( 'test', new TestApiHandler() );
// Add initial notifications // Add initial notifications
model.addItems( items ); model.addItems( items );
@ -151,7 +152,6 @@
QUnit.test( 'Clearing notifications', function ( assert ) { QUnit.test( 'Clearing notifications', function ( assert ) {
var i, ilen, model, actual, test, var i, ilen, model, actual, test,
networkHandler = new mw.echo.dm.NetworkHandler(),
cases = [ cases = [
{ {
prepare: [ prepare: [
@ -182,15 +182,13 @@
assert.expect( cases.length ); assert.expect( cases.length );
for ( i = 0, ilen = cases.length; i < ilen; i++ ) { for ( i = 0, ilen = cases.length; i < ilen; i++ ) {
model = new mw.echo.dm.NotificationsModel( networkHandler, { model = new mw.echo.dm.NotificationsModel( echoApi, {
type: 'alert', type: 'alert',
source: 'test', source: 'test',
limit: 25, limit: 25,
userLang: 'en' userLang: 'en'
} ); } );
networkHandler.addCustomApiHandler( 'test', new TestApiHandler() );
test = cases[ i ]; test = cases[ i ];
// Run preparation // Run preparation
@ -204,7 +202,6 @@
QUnit.test( 'Changing read/unread status', function ( assert ) { QUnit.test( 'Changing read/unread status', function ( assert ) {
var i, var i,
networkHandler = new mw.echo.dm.NetworkHandler(),
initialItems = [ initialItems = [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ), new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ), new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ),
@ -231,14 +228,13 @@
QUnit.expect( cases.length ); QUnit.expect( cases.length );
cases.forEach( function ( test ) { cases.forEach( function ( test ) {
var model = new mw.echo.dm.NotificationsModel( networkHandler, { var model = new mw.echo.dm.NotificationsModel( echoApi, {
type: 'alert', type: 'alert',
source: 'test', source: 'test',
limit: 25, limit: 25,
userLang: 'en' userLang: 'en'
} ); } );
networkHandler.addCustomApiHandler( 'test', new TestApiHandler() );
model.addItems( test.items ); model.addItems( test.items );
if ( test.markRead ) { if ( test.markRead ) {