jenkins-bot has submitted this change and it was merged. Change subject: Add a NetworkHandler as a central API authority to Echo ......................................................................
Add a NetworkHandler as a central API authority to Echo This is in preparation for dealing with cross-wiki notifications where we may need several types of operations to extract bundled notifications from local and external APIs. Also, renamed files: * mw.echo.dm.AbstractAPIHandler -> mw.echo.dm.APIHandler * mw.echo.dm.APIHandler -> mw.echo.dm.LocalAPIHandler * All API-related handler files moved to their own folder for better organization. Change-Id: Ib730c780ea52c93a6026c5d0b22012b6f39bb50d --- M Resources.php M modules/ext.echo.init.js R modules/viewmodel/handlers/mw.echo.dm.APIHandler.js A modules/viewmodel/handlers/mw.echo.dm.ForeignAPIHandler.js R modules/viewmodel/handlers/mw.echo.dm.LocalAPIHandler.js A modules/viewmodel/handlers/mw.echo.dm.NetworkHandler.js M modules/viewmodel/mw.echo.dm.NotificationsModel.js M tests/qunit/viewmodel/test_mw.echo.dm.NotificationsModel.js 8 files changed, 194 insertions(+), 54 deletions(-) Approvals: Catrope: Looks good to me, approved jenkins-bot: Verified diff --git a/Resources.php b/Resources.php index a63d26e..072adf9 100644 --- a/Resources.php +++ b/Resources.php @@ -87,11 +87,12 @@ 'ext.echo.dm' => $echoResourceTemplate + array( 'scripts' => array( 'viewmodel/mw.echo.dm.js', - 'viewmodel/mw.echo.dm.NotificationItem.js', - 'viewmodel/mw.echo.dm.AbstractAPIHandler.js', - 'viewmodel/mw.echo.dm.APIHandler.js', 'viewmodel/mw.echo.dm.List.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.NetworkHandler.js', + 'viewmodel/mw.echo.dm.NotificationItem.js', 'viewmodel/mw.echo.dm.NotificationList.js', 'viewmodel/mw.echo.dm.NotificationsModel.js', ), diff --git a/modules/ext.echo.init.js b/modules/ext.echo.init.js index 0d84e4f..42c5e77 100644 --- a/modules/ext.echo.init.js +++ b/modules/ext.echo.init.js @@ -46,10 +46,10 @@ // Fire the notification API requests apiRequest = new mw.Api( { ajax: { cache: false } } ).get( $.extend( { notsections: myType }, mw.echo.apiCallParams ) ) - .then( function ( data ) { - mw.track( 'timing.MediaWiki.echo.overlay.api', mw.now() - time ); - return data; - } ); + .then( function ( data ) { + mw.track( 'timing.MediaWiki.echo.overlay.api', mw.now() - time ); + return data; + } ); // Load the ui mw.loader.using( 'ext.echo.ui', function () { @@ -58,10 +58,9 @@ // Load message button and popup if messages exist if ( $existingMessageLink.length ) { messageNotificationsModel = new mw.echo.dm.NotificationsModel( - new mw.echo.dm.APIHandler( { + // Create a network handler + new mw.echo.dm.NetworkHandler( { type: 'message', - limit: 25, - userLang: mw.config.get( 'wgUserLanguage' ), baseParams: mw.echo.apiCallParams } ), { @@ -88,13 +87,11 @@ .text( mw.msg( 'mytalk' ) ); } ); } - // Load alerts popup and button alertNotificationsModel = new mw.echo.dm.NotificationsModel( - new mw.echo.dm.APIHandler( { + // Create a network handler + new mw.echo.dm.NetworkHandler( { type: 'alert', - limit: 25, - userLang: mw.config.get( 'wgUserLanguage' ), baseParams: mw.echo.apiCallParams } ), { diff --git a/modules/viewmodel/mw.echo.dm.AbstractAPIHandler.js b/modules/viewmodel/handlers/mw.echo.dm.APIHandler.js similarity index 77% rename from modules/viewmodel/mw.echo.dm.AbstractAPIHandler.js rename to modules/viewmodel/handlers/mw.echo.dm.APIHandler.js index 1ce0a38..97b6320 100644 --- a/modules/viewmodel/mw.echo.dm.AbstractAPIHandler.js +++ b/modules/viewmodel/handlers/mw.echo.dm.APIHandler.js @@ -14,7 +14,7 @@ * @cfg {string} [type='alert'] Notification type * @cfg {string} [userLang='en'] User language */ - mw.echo.dm.AbstractAPIHandler = function MwEchoDmAPIHandler( config ) { + mw.echo.dm.APIHandler = function MwEchoDmAPIHandler( config ) { config = config || {}; // Mixin constructor @@ -33,8 +33,8 @@ /* Setup */ - OO.initClass( mw.echo.dm.AbstractAPIHandler ); - OO.mixinClass( mw.echo.dm.AbstractAPIHandler, OO.EventEmitter ); + OO.initClass( mw.echo.dm.APIHandler ); + OO.mixinClass( mw.echo.dm.APIHandler, OO.EventEmitter ); /** * Fetch notifications from the API. @@ -46,7 +46,7 @@ * @return {jQuery.Promise} A promise that resolves with an object containing the * notification items */ - mw.echo.dm.AbstractAPIHandler.prototype.fetchNotifications = null; + mw.echo.dm.APIHandler.prototype.fetchNotifications = null; /** * Update the seen timestamp @@ -55,7 +55,7 @@ * an array of both. * @return {jQuery.Promise} A promise that resolves with the seen timestamp */ - mw.echo.dm.AbstractAPIHandler.prototype.updateSeenTime = null; + mw.echo.dm.APIHandler.prototype.updateSeenTime = null; /** * Mark all notifications as read @@ -63,7 +63,7 @@ * @return {jQuery.Promise} A promise that resolves when all notifications * are marked as read. */ - mw.echo.dm.AbstractAPIHandler.prototype.markAllRead = null; + mw.echo.dm.APIHandler.prototype.markAllRead = null; /** * Update the read status of a notification item in the API @@ -72,7 +72,7 @@ * @return {jQuery.Promise} A promise that resolves when the notifications * are marked as read. */ - mw.echo.dm.AbstractAPIHandler.prototype.markItemRead = null; + mw.echo.dm.APIHandler.prototype.markItemRead = null; /** * Query the API for unread count of the notifications in this model @@ -80,14 +80,14 @@ * @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched * and the badge label is updated. */ - mw.echo.dm.AbstractAPIHandler.prototype.fetchUnreadCount = null; + 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.AbstractAPIHandler.prototype.isFetchingNotifications = function () { + mw.echo.dm.APIHandler.prototype.isFetchingNotifications = function () { return !!this.fetchNotificationsPromise; }; @@ -96,7 +96,7 @@ * * @return {boolean} The model is in API error state */ - mw.echo.dm.AbstractAPIHandler.prototype.isFetchingErrorState = function () { + mw.echo.dm.APIHandler.prototype.isFetchingErrorState = function () { return !!this.apiErrorState; }; @@ -105,7 +105,7 @@ * @return {jQuery.Promise} Promise that is resolved when notifications are * fetched from the API. */ - mw.echo.dm.AbstractAPIHandler.prototype.getFetchNotificationPromise = function () { + mw.echo.dm.APIHandler.prototype.getFetchNotificationPromise = function () { return this.fetchNotificationsPromise; }; @@ -114,7 +114,7 @@ * * @return {Object} Base API params */ - mw.echo.dm.AbstractAPIHandler.prototype.getBaseParams = function () { + mw.echo.dm.APIHandler.prototype.getBaseParams = function () { return this.baseParams; }; } )( mediaWiki ); diff --git a/modules/viewmodel/handlers/mw.echo.dm.ForeignAPIHandler.js b/modules/viewmodel/handlers/mw.echo.dm.ForeignAPIHandler.js new file mode 100644 index 0000000..3f0de65 --- /dev/null +++ b/modules/viewmodel/handlers/mw.echo.dm.ForeignAPIHandler.js @@ -0,0 +1,25 @@ +( 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 ); diff --git a/modules/viewmodel/mw.echo.dm.APIHandler.js b/modules/viewmodel/handlers/mw.echo.dm.LocalAPIHandler.js similarity index 73% rename from modules/viewmodel/mw.echo.dm.APIHandler.js rename to modules/viewmodel/handlers/mw.echo.dm.LocalAPIHandler.js index 7818daa..0bc500a 100644 --- a/modules/viewmodel/mw.echo.dm.APIHandler.js +++ b/modules/viewmodel/handlers/mw.echo.dm.LocalAPIHandler.js @@ -3,28 +3,28 @@ * Notification API handler * * @class - * @extends mw.echo.dm.AbstractAPIHandler + * @extends mw.echo.dm.APIHandler * * @constructor * @param {Object} [config] Configuration object */ - mw.echo.dm.APIHandler = function MwEchoDmAPIHandler( config ) { + mw.echo.dm.LocalAPIHandler = function MwEchoDmLocalAPIHandler( config ) { config = config || {}; // Parent constructor - mw.echo.dm.APIHandler.parent.call( this, config ); + mw.echo.dm.LocalAPIHandler.parent.call( this, config ); this.api = new mw.Api( { ajax: { cache: false } } ); }; /* Setup */ - OO.inheritClass( mw.echo.dm.APIHandler, mw.echo.dm.AbstractAPIHandler ); + OO.inheritClass( mw.echo.dm.LocalAPIHandler, mw.echo.dm.APIHandler ); /** * @inheritdoc */ - mw.echo.dm.APIHandler.prototype.fetchNotifications = function ( apiPromise ) { + mw.echo.dm.LocalAPIHandler.prototype.fetchNotifications = function ( apiPromise ) { var helper = this, params = $.extend( { notsections: this.type }, this.getBaseParams() ); @@ -46,7 +46,7 @@ /** * @inheritdoc */ - mw.echo.dm.APIHandler.prototype.updateSeenTime = function ( type ) { + mw.echo.dm.LocalAPIHandler.prototype.updateSeenTime = function ( type ) { type = type || this.type; return this.api.postWithToken( 'edit', { @@ -63,12 +63,12 @@ /** * @inheritdoc */ - mw.echo.dm.APIHandler.prototype.markAllRead = function () { + mw.echo.dm.LocalAPIHandler.prototype.markAllRead = function () { var model = this, data = { action: 'echomarkread', uselang: this.userLang, - sections: this.type + sections: $.isArray( this.type ) ? this.type.join( '|' ) : this.type }; return this.api.postWithToken( 'edit', data ) @@ -80,7 +80,7 @@ /** * @inheritdoc */ - mw.echo.dm.APIHandler.prototype.markItemRead = function ( itemId ) { + mw.echo.dm.LocalAPIHandler.prototype.markItemRead = function ( itemId ) { var model = this, data = { action: 'echomarkread', @@ -97,11 +97,11 @@ /** * @inheritdoc */ - mw.echo.dm.APIHandler.prototype.fetchUnreadCount = function () { + mw.echo.dm.LocalAPIHandler.prototype.fetchUnreadCount = function () { var apiData = { action: 'query', meta: 'notifications', - notsections: this.type, + notsections: $.isArray( this.type ) ? this.type.join( '|' ) : this.type, notmessageunreadfirst: 1, notlimit: this.limit, notprop: 'index|count', diff --git a/modules/viewmodel/handlers/mw.echo.dm.NetworkHandler.js b/modules/viewmodel/handlers/mw.echo.dm.NetworkHandler.js new file mode 100644 index 0000000..b1e90aa --- /dev/null +++ b/modules/viewmodel/handlers/mw.echo.dm.NetworkHandler.js @@ -0,0 +1,92 @@ +( 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 ); + + /* 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.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} isExternal Is an external API + * @throws {Error} If no URL was given for a foreign API + */ + mw.echo.dm.NetworkHandler.prototype.addApiHandler = function ( name, config, isExternal ) { + var apiConfig; + + if ( !this.handlers[ name ] ) { + apiConfig = $.extend( true, {}, { baseParams: this.baseParams, type: this.getType() }, config ); + + if ( isExternal ) { + if ( !config.url ) { + throw new Error( 'External 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 ); diff --git a/modules/viewmodel/mw.echo.dm.NotificationsModel.js b/modules/viewmodel/mw.echo.dm.NotificationsModel.js index 4904297..874f19f 100644 --- a/modules/viewmodel/mw.echo.dm.NotificationsModel.js +++ b/modules/viewmodel/mw.echo.dm.NotificationsModel.js @@ -6,14 +6,16 @@ * @mixins OO.EventEmitter * * @constructor - * @param {mw.echo.dm.AbstractAPIHandler} apiHandler API handler + * @param {mw.echo.dm.NetworkHandler} networkHandler Network handler * @param {Object} [config] Configuration object * @cfg {string|string[]} [type='alert'] Notification type 'alert', 'message' * or an array [ 'alert', 'message' ] + * @cfg {string} [source='local'] Model source, 'local' or some symbolic name identifying + * the source of the notification items for the network handler. * @cfg {number} [limit=25] Notification limit * @cfg {string} [userLang] User language */ - mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( apiHandler, config ) { + mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( networkHandler, config ) { config = config || {}; // Mixin constructor @@ -23,8 +25,9 @@ mw.echo.dm.SortedList.call( this ); this.type = config.type || 'alert'; + this.source = config.source || 'local'; - this.apiHandler = apiHandler; + this.networkHandler = networkHandler; this.seenTime = mw.config.get( 'wgEchoSeenTime' ) || {}; @@ -244,7 +247,7 @@ } this.emit( 'updateSeenTime' ); - return this.apiHandler.updateSeenTime( type ) + return this.getApi().updateSeenTime( type ) .then( this.setSeenTime.bind( this ) ); }; @@ -261,7 +264,7 @@ return $.Deferred().resolve( 0 ).promise(); } - return this.apiHandler.markAllRead() + return this.getApi().markAllRead() .then( function () { var i, len, items = model.unreadNotifications.getItems(); @@ -286,7 +289,7 @@ return $.Deferred().resolve( 0 ).promise(); } - return this.apiHandler.markItemRead( itemId ); + return this.getApi().markItemRead( itemId ); }; /** @@ -304,7 +307,7 @@ // Rebuild the notifications promise either when it is null or when // it exists in a failed state - return this.apiHandler.fetchNotifications( apiPromise ) + return this.getApi().fetchNotifications( apiPromise ) .then( function ( result ) { var notifData, i, len, t, tlen, $content, notificationModel, types, @@ -410,7 +413,7 @@ * and the badge label is updated. */ mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi = function () { - return this.apiHandler.fetchUnreadCount(); + return this.getApi().fetchUnreadCount(); }; /** @@ -419,7 +422,7 @@ * @return {boolean} The model is in the process of fetching from the API */ mw.echo.dm.NotificationsModel.prototype.isFetchingNotifications = function () { - return this.apiHandler.isFetchingNotifications(); + return this.getApi().isFetchingNotifications(); }; /** @@ -428,7 +431,7 @@ * @return {boolean} The model is in api error state */ mw.echo.dm.NotificationsModel.prototype.isFetchingErrorState = function () { - return this.apiHandler.isFetchingErrorState(); + return this.getApi().isFetchingErrorState(); }; /** @@ -437,6 +440,16 @@ * fetched from the API. */ mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise = function () { - return this.apiHandler.getFetchNotificationPromise(); + return this.getApi().getFetchNotificationPromise(); }; + + /** + * 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 ); + }; + } )( mediaWiki, jQuery ); diff --git a/tests/qunit/viewmodel/test_mw.echo.dm.NotificationsModel.js b/tests/qunit/viewmodel/test_mw.echo.dm.NotificationsModel.js index 7a6e53d..3a42174 100644 --- a/tests/qunit/viewmodel/test_mw.echo.dm.NotificationsModel.js +++ b/tests/qunit/viewmodel/test_mw.echo.dm.NotificationsModel.js @@ -22,7 +22,7 @@ TestApiHandler.parent.call( this ); } /* Setup */ - OO.inheritClass( TestApiHandler, mw.echo.dm.AbstractAPIHandler ); + OO.inheritClass( TestApiHandler, mw.echo.dm.APIHandler ); // Override api call TestApiHandler.prototype.markItemRead = function () { return $.Deferred().resolve( 0 ); @@ -88,12 +88,15 @@ cases.forEach( function ( test ) { var r, runCase, runItem, - model = new mw.echo.dm.NotificationsModel( new TestApiHandler(), { + networkHandler = new mw.echo.dm.NetworkHandler(), + model = new mw.echo.dm.NotificationsModel( networkHandler, { type: 'alert', + source: 'test', limit: 25, userLang: 'en' } ); + networkHandler.addCustomApiHandler( 'test', new TestApiHandler() ); model.addItems( test.items ); if ( test.add ) { @@ -112,8 +115,10 @@ } ); QUnit.test( 'Deleting notifications', 2, function ( assert ) { - var model = new mw.echo.dm.NotificationsModel( new TestApiHandler(), { + var networkHandler = new mw.echo.dm.NetworkHandler(), + model = new mw.echo.dm.NotificationsModel( networkHandler, { type: 'alert', + source: 'test', limit: 25, userLang: 'en' } ), @@ -130,6 +135,7 @@ new mw.echo.dm.NotificationItem( 10, { content: '10', timestamp: '20150828172900' } ) ]; + networkHandler.addCustomApiHandler( 'test', new TestApiHandler() ); // Add initial notifications model.addItems( items ); @@ -145,6 +151,7 @@ QUnit.test( 'Clearing notifications', function ( assert ) { var i, ilen, model, actual, test, + networkHandler = new mw.echo.dm.NetworkHandler(), cases = [ { prepare: [ @@ -175,11 +182,14 @@ assert.expect( cases.length ); for ( i = 0, ilen = cases.length; i < ilen; i++ ) { - model = new mw.echo.dm.NotificationsModel( new TestApiHandler(), { + model = new mw.echo.dm.NotificationsModel( networkHandler, { type: 'alert', + source: 'test', limit: 25, userLang: 'en' } ); + + networkHandler.addCustomApiHandler( 'test', new TestApiHandler() ); test = cases[ i ]; @@ -194,6 +204,7 @@ QUnit.test( 'Changing read/unread status', function ( assert ) { var i, + networkHandler = new mw.echo.dm.NetworkHandler(), initialItems = [ new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ), new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ), @@ -220,13 +231,14 @@ QUnit.expect( cases.length ); cases.forEach( function ( test ) { - var apiHandler = new TestApiHandler(), - model = new mw.echo.dm.NotificationsModel( apiHandler, { + var model = new mw.echo.dm.NotificationsModel( networkHandler, { type: 'alert', + source: 'test', limit: 25, userLang: 'en' } ); + networkHandler.addCustomApiHandler( 'test', new TestApiHandler() ); model.addItems( test.items ); if ( test.markRead ) { -- To view, visit https://gerrit.wikimedia.org/r/252597 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ib730c780ea52c93a6026c5d0b22012b6f39bb50d Gerrit-PatchSet: 10 Gerrit-Project: mediawiki/extensions/Echo Gerrit-Branch: master Gerrit-Owner: Mooeypoo <mor...@gmail.com> Gerrit-Reviewer: Catrope <roan.katt...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits