Jdlrobson has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/237394

Change subject: WIP Hygiene: Goodbye custom event emitter and class code
......................................................................

WIP Hygiene: Goodbye custom event emitter and class code

Changes:
* Removes EventEmitter code
* All classes previously inheriting from EventEmitter now inherit
from class.
* ModuleLoader in mobile.modules now depends on OO. OOJS now loads
before it
* Remove _parent magic: Previously a View magically inherited defaults and 
templatePartials from
its parent. This was a little confusing and broken, as it only worked for
the immediate parent. Let's remove this code and bring ourselves a little
closer to oojs! This now means you must use $.extend explicitly on 
templatePartials and
defaults
* extend function is now moved from Class to View. Will be deprecated later to 
make
us more consistent.

Change-Id: I5374b2384b1e464cc5312b95bb482ed79f1df70e
---
M includes/Resources.php
M resources/mobile.abusefilter/AbuseFilterOverlay.js
M resources/mobile.betaoptin/BetaOptinPanel.js
M resources/mobile.categories.overlays/CategoryAddOverlay.js
M resources/mobile.categories.overlays/CategoryOverlay.js
M resources/mobile.contentOverlays/PointerOverlay.js
M resources/mobile.drawers/CtaDrawer.js
M resources/mobile.drawers/Drawer.js
A resources/mobile.foreignApi/JSONPForeignApi.js
A resources/mobile.gallery/PhotoListApiGateway.js
A resources/mobile.gallery/test_PhotoListApiGateway.js
M resources/mobile.infiniteScroll/InfiniteScroll.js
M resources/mobile.issues/CleanupOverlay.js
M resources/mobile.languages/LanguageOverlay.js
M resources/mobile.loggingSchemas/SchemaMobileWeb.js
M resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js
M resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js
M resources/mobile.loggingSchemas/SchemaMobileWebEditing.js
M resources/mobile.loggingSchemas/SchemaMobileWebSearch.js
M resources/mobile.loggingSchemas/SchemaMobileWebWatching.js
M resources/mobile.modules/modules.js
M resources/mobile.nearby/Nearby.js
M resources/mobile.nearby/NearbyApi.js
A resources/mobile.nearby/NearbyApiGateway.js
M resources/mobile.notifications.overlay/NotificationsOverlay.js
M resources/mobile.oo/Class.js
D resources/mobile.oo/eventemitter.js
M resources/mobile.search/MobileWebSearchLogger.js
M resources/mobile.search/SearchOverlay.js
M resources/mobile.startup/PageApi.js
M resources/mobile.startup/Router.js
M resources/mobile.startup/Schema.js
M resources/mobile.startup/api.js
M resources/mobile.swipe/Swipe.js
M resources/mobile.talk.overlays/TalkOverlay.js
M resources/mobile.talk.overlays/TalkSectionAddOverlay.js
M resources/mobile.view/View.js
A tests/qunit/mobile.nearby/test_NearbyApiGateway.js
D tests/qunit/mobile.oo/test_Class.js
D tests/qunit/mobile.oo/test_eventemitter.js
M tests/qunit/mobile.overlays/test_Overlay.js
M tests/qunit/mobile.startup/test_OverlayManager.js
M tests/qunit/mobile.startup/test_Schema.js
M tests/qunit/mobile.view/test_View.js
44 files changed, 733 insertions(+), 289 deletions(-)


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

diff --git a/includes/Resources.php b/includes/Resources.php
index c0b9b74..f537f3c 100644
--- a/includes/Resources.php
+++ b/includes/Resources.php
@@ -230,6 +230,9 @@
 
 $wgResourceModules = array_merge( $wgResourceModules, array(
        'mobile.modules' => $wgMFResourceFileModuleBoilerplate + array(
+               'dependencies' => array(
+                       'oojs',
+               ),
                'scripts' => array(
                        'resources/mobile.modules/modules.js',
                ),
@@ -241,7 +244,6 @@
                ),
                'scripts' => array(
                        'resources/mobile.oo/Class.js',
-                       'resources/mobile.oo/eventemitter.js',
                ),
        ),
        'mobile.view' => $wgMFResourceFileModuleBoilerplate + array(
diff --git a/resources/mobile.abusefilter/AbuseFilterOverlay.js 
b/resources/mobile.abusefilter/AbuseFilterOverlay.js
index e1ddab0..0a8bd69 100644
--- a/resources/mobile.abusefilter/AbuseFilterOverlay.js
+++ b/resources/mobile.abusefilter/AbuseFilterOverlay.js
@@ -1,4 +1,4 @@
-( function ( M ) {
+( function ( M, $ ) {
        var AbuseFilterOverlay,
                Button = M.require( 'Button' ),
                Overlay = M.require( 'Overlay' );
@@ -15,17 +15,17 @@
                 * @cfg {Object} defaults Default options hash.
                 * @cfg {Object} defaults.confirmButton options for a confirm 
Button
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        confirmButton: new Button( {
                                additionalClassNames: 'cancel',
                                progressive: true,
                                label: mw.msg( 
'mobile-frontend-photo-ownership-confirm' )
                        } ).options
-               },
-               templatePartials: {
+               } ),
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        button: Button.prototype.template,
                        content: mw.template.get( 'mobile.abusefilter', 
'Overlay.hogan' )
-               },
+               } ),
                className: 'overlay abusefilter-overlay',
 
                /** @inheritdoc */
@@ -37,4 +37,4 @@
        } );
 
        M.define( 'modules/editor/AbuseFilterOverlay', AbuseFilterOverlay );
-}( mw.mobileFrontend ) );
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.betaoptin/BetaOptinPanel.js 
b/resources/mobile.betaoptin/BetaOptinPanel.js
index 22cf691..3f81ced 100644
--- a/resources/mobile.betaoptin/BetaOptinPanel.js
+++ b/resources/mobile.betaoptin/BetaOptinPanel.js
@@ -10,11 +10,11 @@
         */
        BetaOptinPanel = Panel.extend( {
                className: 'panel panel-inline visible',
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Panel.prototype.templatePartials, {
                        button: Button.prototype.template
-               },
+               } ),
                template: mw.template.get( 'mobile.betaoptin', 'Panel.hogan' ),
-               defaults: {
+               defaults: $.extend( {}, Panel.prototype.defaults, {
                        postUrl: undefined,
                        editToken: mw.user.tokens.get( 'editToken' ),
                        enableImages: mw.config.get( 'wgImagesDisabled' ) ? 0 : 
1,
@@ -30,7 +30,7 @@
                                        label: mw.msg( 
'mobile-frontend-panel-cancel' )
                                } ).options
                        ]
-               },
+               } ),
                events: $.extend( {}, Panel.prototype.events, {
                        'click .optin': 'onOptin'
                } ),
diff --git a/resources/mobile.categories.overlays/CategoryAddOverlay.js 
b/resources/mobile.categories.overlays/CategoryAddOverlay.js
index 95245b9..c803ef6 100644
--- a/resources/mobile.categories.overlays/CategoryAddOverlay.js
+++ b/resources/mobile.categories.overlays/CategoryAddOverlay.js
@@ -21,11 +21,11 @@
                 * @cfg {String} defaults.waitIcon HTML of the icon that 
displays while a page edit
                 * is being saved.
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        headerButtonsListClassName: 'overlay-action',
                        waitMsg: mw.msg( 'mobile-frontend-categories-add-wait' 
),
                        waitIcon: icons.spinner().toHtmlString()
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
@@ -44,10 +44,10 @@
                /**
                 * @inheritdoc
                 */
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        header: mw.template.get( 'mobile.categories.overlays', 
'CategoryAddOverlayHeader.hogan' ),
                        saveHeader: mw.template.get( 'mobile.editor.common', 
'saveHeader.hogan' )
-               },
+               } ),
 
                /**
                 * @inheritdoc
diff --git a/resources/mobile.categories.overlays/CategoryOverlay.js 
b/resources/mobile.categories.overlays/CategoryOverlay.js
index d3a8a61..0acc32e 100644
--- a/resources/mobile.categories.overlays/CategoryOverlay.js
+++ b/resources/mobile.categories.overlays/CategoryOverlay.js
@@ -21,7 +21,7 @@
                 * @cfg {Array} defaults.headerButtons Objects that will be 
used as defaults for
                 * generating header buttons.
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        heading: mw.msg( 'mobile-frontend-categories-heading' ),
                        subheading: mw.msg( 
'mobile-frontend-categories-subheading' ),
                        headerButtonsListClassName: 'overlay-action',
@@ -32,7 +32,7 @@
                        } ],
                        normalcatlink: mw.msg( 
'mobile-frontend-categories-normal' ),
                        hiddencatlink: mw.msg( 
'mobile-frontend-categories-hidden' )
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
@@ -40,9 +40,9 @@
                /**
                 * @inheritdoc
                 */
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        content: mw.template.get( 'mobile.categories.overlays', 
'CategoryOverlay.hogan' )
-               },
+               } ),
                events: $.extend( {}, Overlay.prototype.events, {
                        'click .catlink': 'onCatlinkClick'
                } ),
diff --git a/resources/mobile.contentOverlays/PointerOverlay.js 
b/resources/mobile.contentOverlays/PointerOverlay.js
index d0090e2..0492e1e 100644
--- a/resources/mobile.contentOverlays/PointerOverlay.js
+++ b/resources/mobile.contentOverlays/PointerOverlay.js
@@ -33,7 +33,7 @@
                 * @cfg {String} [defaults.alignment] Determines where the 
pointer should point to. Valid values 'left' or 'center'
                 * @cfg {String} [defaults.confirmMsg] Label for a confirm 
message.
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        skin: undefined,
                        summary: undefined,
                        cancelMsg: mw.msg( 'mobile-frontend-pointer-dismiss' ),
@@ -41,7 +41,7 @@
                        target: undefined,
                        alignment: 'center',
                        confirmMsg: undefined
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
diff --git a/resources/mobile.drawers/CtaDrawer.js 
b/resources/mobile.drawers/CtaDrawer.js
index ad22608..5f6c7c0 100644
--- a/resources/mobile.drawers/CtaDrawer.js
+++ b/resources/mobile.drawers/CtaDrawer.js
@@ -22,7 +22,7 @@
                 * @cfg {Object} defaults.progressiveButton options for Button 
element for signing in
                 * @cfg {Object} defaults.actionAnchor options for Anchor 
element for signing up
                 */
-               defaults: {
+               defaults: $.extend( {}, Drawer.prototype.defaults, {
                        progressiveButton: new Button( {
                                progressive: true,
                                label: mw.msg( 
'mobile-frontend-watchlist-cta-button-login' )
@@ -35,12 +35,12 @@
                                name: 'arrow-down',
                                additionalClassNames: 'cancel'
                        } ).options
-               },
-               templatePartials: {
+               } ),
+               templatePartials: $.extend( {}, 
Drawer.prototype.templatePartials, {
                        icon: Icon.prototype.template,
                        button: Button.prototype.template,
                        anchor: Anchor.prototype.template
-               },
+               } ),
                template: mw.template.get( 'mobile.drawers', 'Cta.hogan' ),
                /**
                 * @inheritdoc
diff --git a/resources/mobile.drawers/Drawer.js 
b/resources/mobile.drawers/Drawer.js
index ba09a17..7fd52b3 100644
--- a/resources/mobile.drawers/Drawer.js
+++ b/resources/mobile.drawers/Drawer.js
@@ -15,14 +15,14 @@
                 * @cfg {Object} defaults Default options hash.
                 * @cfg {String} defaults.cancelButton HTML of the button that 
closes the drawer.
                 */
-               defaults: {
+               defaults: $.extend( {}, Panel.prototype.defaults, {
                        cancelButton: new Icon( {
                                tagName: 'a',
                                name: 'close-invert',
                                additionalClassNames: 'cancel',
                                label: mw.msg( 'mobile-frontend-overlay-close' )
                        } ).toHtmlString()
-               },
+               } ),
                className: 'drawer position-fixed',
                /**
                 * Defines an element that the Drawer should automatically be 
appended to.
diff --git a/resources/mobile.foreignApi/JSONPForeignApi.js 
b/resources/mobile.foreignApi/JSONPForeignApi.js
new file mode 100644
index 0000000..7fa86d2
--- /dev/null
+++ b/resources/mobile.foreignApi/JSONPForeignApi.js
@@ -0,0 +1,32 @@
+( function ( M, $ ) {
+       /**
+        * Uses JSONP for non-post requests
+        * @class JSONPForeignApi
+        * @extends mw.ForeignApi
+        */
+       function JSONPForeignApi( endpoint, options ) {
+               options = options || {};
+               options.origin = undefined;
+               mw.ForeignApi.call( this, endpoint, options );
+               delete this.defaults.parameters.origin;
+       }
+       OO.inheritClass( JSONPForeignApi, mw.ForeignApi );
+
+       /** @inheritdoc */
+       JSONPForeignApi.prototype.ajax = function ( data, options ) {
+               if ( !options || options.type !== 'POST' ) {
+                       // optional parameter so may need to define it.
+                       options = options || {};
+                       // Fire jsonp where it can be.
+                       options.dataType = 'jsonp';
+                       // explicitly avoid requesting central auth tokens
+       OO.mfExtend( data, $, {}, data, {
+                               centralauthtoken: false
+                       } );
+               }
+               return mw.ForeignApi.prototype.ajax.call( this, data, options );
+       };
+
+       M.define( 'mobile.foreignApi/JSONPForeignApi', JSONPForeignApi );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.gallery/PhotoListApiGateway.js 
b/resources/mobile.gallery/PhotoListApiGateway.js
new file mode 100644
index 0000000..9506c6b
--- /dev/null
+++ b/resources/mobile.gallery/PhotoListApiGateway.js
@@ -0,0 +1,140 @@
+( function ( M, $ ) {
+
+       var IMAGE_WIDTH = mw.config.get( 'wgMFThumbnailSizes' ).small;
+
+       /**
+        * API for retrieving gallery photos
+        * @class PhotoListApi
+        * @param {Object} options
+        * @param {mw.Api} options.api
+        */
+       function PhotoListApiGateway( options ) {
+               this.api = options.api;
+               this.username = options.username;
+               this.category = options.category;
+               this.limit = 10;
+               this.continueParams = {
+                       continue: ''
+               };
+               this.canContinue = true;
+       }
+
+       PhotoListApiGateway.prototype = {
+               /**
+                * Returns a description based on the file name using
+                * a regular expression that strips the file type suffix,
+                * namespace prefix and any
+                * date suffix in format YYYY-MM-DD HH-MM
+                * @method
+                * @private
+                * @param {String} title Title of file
+                * @return {String} Description for file
+                */
+               _getDescription: function ( title ) {
+                       title = title.replace( /\.[^\. ]+$/, '' ); // replace 
filename suffix
+                       // strip namespace: prefix and date suffix from 
remainder
+                       return title.replace( /^[^:]*:/, '' )
+                               .replace( / \d{4}-\d{1,2}-\d{1,2} 
\d{1,2}-\d{1,2}$/, '' );
+               },
+               /**
+                * Returns the value in pixels of a medium thumbnail
+                * @method
+                */
+               getWidth: function () {
+                       return IMAGE_WIDTH;
+               },
+               /**
+                * Extracts image data from api response
+                * @method
+                * @private
+                * @param {Object} page as returned by api request
+                * @return {Object} describing image.
+                */
+               _getImageDataFromPage: function ( page ) {
+                       var img = page.imageinfo[0];
+                       return {
+                               url: img.thumburl,
+                               title: page.title,
+                               timestamp: img.timestamp,
+                               description: this._getDescription( page.title ),
+                               descriptionUrl: img.descriptionurl
+                       };
+               },
+               /**
+                * Get the associated query needed to retrieve images from API 
based
+                * on currently configured options.
+                * @return {Object}
+                */
+               getQuery: function () {
+       OO.mfExtend( var query, $, {
+                               action: 'query',
+                               prop: 'imageinfo',
+                               // FIXME: [API] have to request timestamp since 
api returns an object
+                               // rather than an array thus we need a way to 
sort
+                               iiprop: 'url|timestamp',
+                               iiurlwidth: IMAGE_WIDTH
+                       }, this.continueParams );
+
+                       if ( this.username ) {
+                               $.extend( query, {
+                                       generator: 'allimages',
+                                       gaiuser: this.username,
+                                       gaisort: 'timestamp',
+                                       gaidir: 'descending',
+                                       gailimit: this.limit
+                               } );
+                       } else if ( this.category ) {
+                               $.extend( query, {
+                                       generator: 'categorymembers',
+                                       gcmtitle: 'Category:' + this.category,
+                                       gcmtype: 'file',
+                                       // FIXME [API] a lot of duplication 
follows due to the silly way generators work
+                                       gcmdir: 'descending',
+                                       gcmlimit: this.limit
+                               } );
+                       }
+
+                       return query;
+               },
+               /**
+                * Request photos beginning with the current value of 
endTimestamp
+                * @return {jQuery.Deferred} where parameter is a list of 
JavaScript objects describing an image.
+                */
+               getPhotos: function () {
+                       var self = this,
+                               result = $.Deferred();
+
+                       if ( this.canContinue === true ) {
+                               this.api.ajax( this.getQuery() ).done( function 
( resp ) {
+                                       var photos;
+                                       if ( resp.query && resp.query.pages ) {
+                                               // FIXME: [API] in an ideal 
world imageData would be a sorted array
+                                               photos = $.map( 
resp.query.pages, function ( page ) {
+                                                               return 
self._getImageDataFromPage.call( self, page );
+                                                       } ).sort( function ( a, 
b ) {
+                                                               return 
a.timestamp < b.timestamp ? 1 : -1;
+                                                       } );
+
+                                               if ( resp.hasOwnProperty( 
'continue' ) ) {
+                                                       self.continueParams = 
resp.continue;
+                                               } else {
+                                                       self.canContinue = 
false;
+                                               }
+
+                                               // FIXME: Should reply with a 
list of PhotoItem or Photo classes.
+                                               result.resolve( photos );
+                                       } else {
+                                               result.resolve( [] );
+                                       }
+                               } ).fail( $.proxy( result, 'reject' ) );
+                       } else {
+                               result.resolve( [] );
+                       }
+
+                       return result;
+               }
+       };
+
+       M.define( 'mobile.gallery/PhotoListApiGateway', PhotoListApiGateway )
+               .deprecate( 'modules/gallery/PhotoListApi' );
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.gallery/test_PhotoListApiGateway.js 
b/resources/mobile.gallery/test_PhotoListApiGateway.js
new file mode 100644
index 0000000..fb60d82
--- /dev/null
+++ b/resources/mobile.gallery/test_PhotoListApiGateway.js
@@ -0,0 +1,26 @@
+( function ( M, $ ) {
+
+       var m = M.require( 'modules/gallery/PhotoListApi' );
+
+       QUnit.module( 'MobileFrontend PhotoListApi' );
+
+       QUnit.test( '#getDescription', function ( assert ) {
+               var tests = [
+                       [ 'File:Pirates in SF 2013-04-03 15-44.png', 'Pirates 
in SF' ],
+                       [ 'File:Unpadded 9 pirates in SF 2013-04-03 15-9.png', 
'Unpadded 9 pirates in SF' ],
+                       [ 'File:Jon lies next to volcano 2013-03-18 
13-37.jpeg', 'Jon lies next to volcano' ],
+                       [ 'hello world 37.jpg', 'hello world 37' ],
+                       [ 'hello world again.jpeg', 'hello world again' ],
+                       [ 'Fichier:French Photo Timestamp 2013-04-03 
15-44.jpg', 'French Photo Timestamp' ],
+                       [ 'Fichier:Full stop. Photo.unknownfileextension', 
'Full stop. Photo' ],
+                       [ 'File:No file extension but has a . in the title', 
'No file extension but has a . in the title' ],
+                       [ 'Fichier:French Photo.jpg', 'French Photo' ]
+               ];
+               QUnit.expect( tests.length );
+               $( tests ).each( function ( i ) {
+                       var val = m.prototype._getDescription( this[ 0 ] );
+                       assert.strictEqual( val, this[ 1 ], 'test ' + i );
+               } );
+       } );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.infiniteScroll/InfiniteScroll.js 
b/resources/mobile.infiniteScroll/InfiniteScroll.js
index 08e930d..119ff56 100644
--- a/resources/mobile.infiniteScroll/InfiniteScroll.js
+++ b/resources/mobile.infiniteScroll/InfiniteScroll.js
@@ -1,13 +1,10 @@
 ( function ( M, $ ) {
-
-       var EventEmitter = M.require( 'eventemitter' ),
-               InfiniteScroll;
-
        /**
         * Class to assist a view in implementing infinite scrolling on some DOM
         * element.
         *
         * @class InfiniteScroll
+        * @mixin OO.EventEmitter
         *
         * Use this class in a view to help it do infinite scrolling.
         *
@@ -53,18 +50,20 @@
         *       } );
         *     </code>
         */
-       InfiniteScroll = EventEmitter.extend( {
-               /**
-                * Constructor.
-                * @param {Number} threshold distance in pixels used to 
calculate if scroll
-                * position is near the end of the $el
-                */
-               initialize: function ( threshold ) {
-                       EventEmitter.prototype.initialize.apply( this, 
arguments );
-                       this.threshold = threshold || 100;
-                       this.enabled = true;
-                       this._bindScroll();
-               },
+       /**
+        * Constructor.
+        * @param {Number} threshold distance in pixels used to calculate if 
scroll
+        * position is near the end of the $el
+        */
+       function InfiniteScroll( threshold ) {
+               this.threshold = threshold || 100;
+               this.enabled = true;
+               this._bindScroll();
+               OO.EventEmitter.call( this );
+       }
+       OO.mixinClass( InfiniteScroll, OO.EventEmitter );
+
+       OO.mfExtend( InfiniteScroll, {
                /**
                 * Listen to scroll on window and notify this._onScroll
                 * @method
diff --git a/resources/mobile.issues/CleanupOverlay.js 
b/resources/mobile.issues/CleanupOverlay.js
index ba5c666..262b1e6 100644
--- a/resources/mobile.issues/CleanupOverlay.js
+++ b/resources/mobile.issues/CleanupOverlay.js
@@ -1,4 +1,4 @@
-( function ( M ) {
+( function ( M, $ ) {
        var Overlay = M.require( 'Overlay' ),
                Icon = M.require( 'Icon' ),
                icon = new Icon( {
@@ -14,17 +14,17 @@
         * @extends Overlay
         */
        CleanupOverlay = Overlay.extend( {
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        content: mw.template.get( 'mobile.issues', 
'OverlayContent.hogan' )
-               },
+               } ),
                /**
                 * @inheritdoc
                 * @cfg {Object} defaults Default options hash.
                 * @cfg {String} defaults.className Class name of the 
'cleanup-gray' icon.
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        className: icon.getClassName()
-               },
+               } ),
                /** @inheritdoc */
                initialize: function ( options ) {
                        options.heading = '<strong>' + options.headingText + 
'</strong>';
@@ -32,4 +32,4 @@
                }
        } );
        M.define( 'modules/issues/CleanupOverlay', CleanupOverlay );
-}( mw.mobileFrontend ) );
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.languages/LanguageOverlay.js 
b/resources/mobile.languages/LanguageOverlay.js
index 3c43b52..19318a2 100644
--- a/resources/mobile.languages/LanguageOverlay.js
+++ b/resources/mobile.languages/LanguageOverlay.js
@@ -17,10 +17,10 @@
                 * @cfg {String} defaults.placeholder Placeholder text for the 
search input.
                 * @cfg {Object} defaults.languages a list of languages with 
keys {langname, lang, title, url}
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        heading: mw.msg( 'mobile-frontend-language-heading' ),
                        placeholder: mw.msg( 
'mobile-frontend-language-site-choose' )
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
@@ -28,9 +28,9 @@
                /**
                 * @inheritdoc
                 */
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        content: mw.template.get( 'mobile.languages', 
'LanguageOverlay.hogan' )
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWeb.js 
b/resources/mobile.loggingSchemas/SchemaMobileWeb.js
index b4e8ef6..8580000 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWeb.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWeb.js
@@ -1,13 +1,15 @@
 ( function ( M, $ ) {
-       var SchemaMobileWeb,
-               Schema = M.require( 'Schema' ),
+       var Schema = M.require( 'Schema' ),
                context = M.require( 'context' );
 
+       function SchemaMobileWeb() {
+               Schema.apply( this, arguments );
+       }
        /**
         * @class SchemaMobileWeb
         * @extends Schema
         */
-       SchemaMobileWeb = Schema.extend( {
+       OO.mfExtend( SchemaMobileWeb, Schema, {
                /**
                 * @inheritdoc
                 *
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js 
b/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js
index d66275c..922424c 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWebBrowse.js
@@ -1,13 +1,15 @@
 ( function ( M ) {
-       var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ),
-               SchemaMobileWebBrowse;
+       var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' );
 
        /**
         * Implement schema defined at 
https://meta.wikimedia.org/wiki/Schema:MobileWebBrowse
         * @class SchemaMobileWebBrowse
         * @extends SchemaMobileWeb
         */
-       SchemaMobileWebBrowse = SchemaMobileWeb.extend( {
+       function SchemaMobileWebBrowse() {
+               SchemaMobileWeb.apply( this, arguments );
+       }
+       OO.mfExtend( SchemaMobileWebBrowse, SchemaMobileWeb, {
                /** @inheritdoc **/
                name: 'MobileWebBrowse'
        } );
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js 
b/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js
index 7eb249c..754761f 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWebClickTracking.js
@@ -1,6 +1,5 @@
 ( function ( M, $ ) {
-       var SchemaMobileWebClickTracking,
-               SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ),
+       var SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' ),
                user = M.require( 'user' ),
                s = M.require( 'settings' );
 
@@ -49,7 +48,10 @@
         * @class SchemaMobileWebClickTracking
         * @extends Schema
         */
-       SchemaMobileWebClickTracking = SchemaMobileWeb.extend( {
+       function SchemaMobileWebClickTracking() {
+               SchemaMobileWeb.apply( this, arguments );
+       }
+       OO.mfExtend( SchemaMobileWebClickTracking, SchemaMobileWeb, {
                /**
                 * @inheritdoc
                 *
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js 
b/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js
index 1877de8..94b482b 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWebEditing.js
@@ -1,13 +1,15 @@
 ( function ( M, $ ) {
-       var SchemaMobileWebEditing,
-               user = M.require( 'user' ),
+       var user = M.require( 'user' ),
                SchemaMobileWeb = M.require( 'loggingSchemas/SchemaMobileWeb' );
 
        /**
         * @class SchemaMobileWebEditing
         * @extends Schema
         */
-       SchemaMobileWebEditing = SchemaMobileWeb.extend( {
+       function SchemaMobileWebEditing() {
+               SchemaMobileWeb.apply( this, arguments );
+       }
+       OO.mfExtend( SchemaMobileWebEditing, SchemaMobileWeb, {
                /** @inheritdoc **/
                name: 'MobileWebEditing',
                /**
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js 
b/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js
index e367e91..6140583 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWebSearch.js
@@ -1,13 +1,16 @@
 ( function ( M, $ ) {
        var Schema = M.require( 'Schema' ),
-               SchemaMobileWebSearch,
                context = M.require( 'context' );
+
+       function SchemaMobileWebSearch() {
+               Schema.apply( this, arguments );
+       }
 
        /**
         * @class SchemaMobileWebSearch
         * @extends Schema
         */
-       SchemaMobileWebSearch = Schema.extend( {
+       OO.mfExtend( SchemaMobileWebSearch, Schema, {
                /** @inheritdoc **/
                name: 'MobileWebSearch',
                /** @inheritdoc */
diff --git a/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js 
b/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js
index 2bb6683..150624d 100644
--- a/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js
+++ b/resources/mobile.loggingSchemas/SchemaMobileWebWatching.js
@@ -7,7 +7,10 @@
         * @class SchemaMobileWebWatching
         * @extends Schema
         */
-       SchemaMobileWebWatching = SchemaMobileWeb.extend( {
+       function SchemaMobileWebWatching() {
+               SchemaMobileWeb.apply( this, arguments );
+       }
+       OO.mfExtend( SchemaMobileWebWatching, SchemaMobileWeb, {
                /** @inheritdoc **/
                name: 'MobileWebWatching',
                /**
diff --git a/resources/mobile.modules/modules.js 
b/resources/mobile.modules/modules.js
index 13dec79..73065d0 100644
--- a/resources/mobile.modules/modules.js
+++ b/resources/mobile.modules/modules.js
@@ -1,6 +1,4 @@
 ( function () {
-       var loader;
-
        /**
         * Class for managing modules
         *
@@ -8,6 +6,7 @@
         * ResourceLoader modules).
         *
         * @class ModuleLoader
+        * @extends OO.EventEmitter
         */
        function ModuleLoader() {
                /**
@@ -15,6 +14,7 @@
                 * @private
                 */
                this._register = {};
+               OO.EventEmitter.call( this );
        }
 
        ModuleLoader.prototype = {
@@ -75,8 +75,7 @@
                        mw.log.deprecate( this._register, id, obj, depreacteMsg 
);
                }
        };
-
-       loader = new ModuleLoader();
+       OO.mixinClass( ModuleLoader, OO.EventEmitter );
 
        /**
         *
@@ -85,27 +84,7 @@
         * @class mw.mobileFrontend
         * @singleton
         */
-       mw.mobileFrontend = {
-               /**
-                * @see ModuleLoader#define
-                * @return {Object}
-                */
-               define: function () {
-                       return loader.define.apply( loader, arguments );
-               },
-               /**
-                * @see ModuleLoader#require
-                */
-               require: function () {
-                       return loader.require.apply( loader, arguments );
-               },
-               /**
-                * @see ModuleLoader#deprecate
-                */
-               deprecate: function () {
-                       return loader.deprecate.apply( loader, arguments );
-               }
-       };
+       mw.mobileFrontend = new ModuleLoader();
 
        // inception to support testing (!!)
        mw.mobileFrontend.define( 'ModuleLoader', ModuleLoader );
diff --git a/resources/mobile.nearby/Nearby.js 
b/resources/mobile.nearby/Nearby.js
index 0db7616..8870a65 100644
--- a/resources/mobile.nearby/Nearby.js
+++ b/resources/mobile.nearby/Nearby.js
@@ -36,10 +36,10 @@
                                msg: mw.msg( 
'mobile-frontend-nearby-requirements-guidance' )
                        }
                },
-               templatePartials: {
+               templatePartials: $.extend( {}, 
WatchstarPageList.prototype.templatePartials, {
                        pageList: WatchstarPageList.prototype.template,
                        messageBox: MessageBox.prototype.template
-               },
+               } ),
                template: mw.template.get( 'mobile.nearby', 'Nearby.hogan' ),
                /**
                 * @inheritdoc
@@ -48,12 +48,12 @@
                 * @cfg {String} defaults.spinner HTML of the spinner icon with 
a tooltip that
                 * tells the user that their location is being looked up
                 */
-               defaults: {
+               defaults: $.extend( {}, WatchstarPageList.prototype.defaults, {
                        errorOptions: undefined,
                        spinner: icons.spinner( {
                                title: mw.msg( 'mobile-frontend-nearby-loading' 
)
                        } ).toHtmlString()
-               },
+               } ),
 
                /**
                 * Obtain users current location and return a deferred object 
with the
diff --git a/resources/mobile.nearby/NearbyApi.js 
b/resources/mobile.nearby/NearbyApi.js
index 11bea30..82d8849 100644
--- a/resources/mobile.nearby/NearbyApi.js
+++ b/resources/mobile.nearby/NearbyApi.js
@@ -53,7 +53,10 @@
         * @class NearbyApi
         * @extends Api
         */
-       NearbyApi = Api.extend( {
+       function NearbyApi() {
+               this.initialize.apply( this, arguments );
+       }
+       OO.mfExtend( NearbyApi, Api, {
                apiUrl: endpoint || Api.prototype.apiUrl,
                /**
                 * Returns a human readable string stating the distance in 
meters or kilometers
diff --git a/resources/mobile.nearby/NearbyApiGateway.js 
b/resources/mobile.nearby/NearbyApiGateway.js
new file mode 100644
index 0000000..f8178d1
--- /dev/null
+++ b/resources/mobile.nearby/NearbyApiGateway.js
@@ -0,0 +1,205 @@
+( function ( M, $ ) {
+
+       var limit = 50,
+               Page = M.require( 'Page' ),
+               ns = mw.config.get( 'wgMFContentNamespace' );
+
+       /**
+        * FIXME: Api should surely know this and return it in response to save 
us the hassle
+        * FIXME: Add some tests :)
+        * Apply the Haversine formula ( 
https://en.wikipedia.org/wiki/Haversine_formula ) and calculate the distance
+        * between two points as the crow flies.
+        * @method
+        * @ignore
+        * @param {Object} from with latitude and longitude keys
+        * @param {Object} to with latitude and longitude keys
+        * @return {Number} distance in kilometers
+        */
+       function calculateDistance( from, to ) {
+               var distance, a,
+                       toRadians = Math.PI / 180,
+                       deltaLat, deltaLng,
+                       startLat, endLat,
+                       haversinLat, haversinLng,
+                       radius = 6378; // radius of Earth in km
+
+               if ( from.latitude === to.latitude && from.longitude === 
to.longitude ) {
+                       distance = 0;
+               } else {
+                       deltaLat = ( to.longitude - from.longitude ) * 
toRadians;
+                       deltaLng = ( to.latitude - from.latitude ) * toRadians;
+                       startLat = from.latitude * toRadians;
+                       endLat = to.latitude * toRadians;
+
+                       haversinLat = Math.sin( deltaLat / 2 ) * Math.sin( 
deltaLat / 2 );
+                       haversinLng = Math.sin( deltaLng / 2 ) * Math.sin( 
deltaLng / 2 );
+
+                       a = haversinLat + Math.cos( startLat ) * Math.cos( 
endLat ) * haversinLng;
+                       return 2 * radius * Math.asin( Math.sqrt( a ) );
+               }
+               return distance;
+       }
+
+       /**
+        * API for retrieving nearby pages
+        * @class NearbyApiGateway
+        * @param {Object} options
+        * @param {mw.Api} options.api
+        */
+       function NearbyApiGateway( options ) {
+               this.api = options.api;
+       }
+
+       NearbyApiGateway.prototype = {
+               /**
+                * Returns a human readable string stating the distance in 
meters or kilometers
+                * depending on size.
+                * @method
+                * @private
+                * @param {Number} d The distance in meters.
+                * @return {String} message stating how far the user is from 
the point of interest.
+                */
+               _distanceMessage: function ( d ) {
+                       var msg = 'mobile-frontend-nearby-distance';
+                       if ( d < 1 ) {
+                               d *= 100;
+                               d = Math.ceil( d ) * 10;
+                               if ( d === 1000 ) {
+                                       d = 1;
+                               } else {
+                                       msg = 
'mobile-frontend-nearby-distance-meters';
+                               }
+                       } else {
+                               if ( d > 2 ) {
+                                       d *= 10;
+                                       d = Math.ceil( d ) / 10;
+                                       d = d.toFixed( 1 );
+                               } else {
+                                       d *= 100;
+                                       d = Math.ceil( d ) / 100;
+                                       d = d.toFixed( 2 );
+                               }
+                       }
+                       return mw.msg( msg, mw.language.convertNumber( d ) );
+               },
+               /**
+                * Returns a list of pages around a given point
+                * @method
+                * @param {Object} coords In form { latitude: 0, longitude: 2 }
+                * @param {Number} range Number of meters to perform a 
geosearch for
+                * @param {String} exclude Name of a title to exclude from the 
list of results
+                * @return {jQuery.Deferred} Object taking list of pages as 
argument
+                */
+               getPages: function ( coords, range, exclude ) {
+                       return this._search( {
+                               ggscoord: [ coords.latitude, coords.longitude ]
+                       }, range, exclude );
+               },
+
+               /**
+                * Gets the pages around a page. It excludes itself from the 
search
+                * @method
+                * @param {String} page Page title like "W_San_Francisco"
+                * @param {Number} range Number of meters to perform a 
geosearch for
+                * @return {jQuery.Deferred} Object taking list of pages as 
argument
+                */
+               getPagesAroundPage: function ( page, range ) {
+                       return this._search( {
+                               ggspage: page
+                       }, range, page );
+               },
+
+               /**
+                * Searches for pages nearby
+                * @method
+                * @private
+                * @param {Object} params Parameters to use for searching
+                * @param {Number} range Number of meters to perform a 
geosearch for
+                * @param {String} exclude Name of a title to exclude from the 
list of results
+                * @return {jQuery.Deferred} Object taking list of pages as 
argument
+                */
+               _search: function ( params, range, exclude ) {
+                       var loc, requestParams,
+                               d = $.Deferred(),
+                               self = this;
+
+                       requestParams = {
+                               action: 'query',
+                               colimit: 'max',
+                               prop: 'pageimages|coordinates',
+                               pithumbsize: mw.config.get( 
'wgMFThumbnailSizes' ).small,
+                               pilimit: limit,
+                               generator: 'geosearch',
+                               ggsradius: range,
+                               ggsnamespace: ns,
+                               ggslimit: limit,
+                               formatversion: 2
+                       };
+                       $.extend( requestParams, params );
+
+                       this.api.ajax( requestParams ).then( function ( resp ) {
+                               var pages;
+                               if ( resp.query ) {
+                                       pages = resp.query.pages || [];
+                               } else {
+                                       pages = [];
+                               }
+
+                               // If we have coordinates then set them so that 
the results are sorted by
+                               // distance
+                               if ( params.ggscoord ) {
+                                       loc = {
+                                               latitude: params.ggscoord[0],
+                                               longitude: params.ggscoord[1]
+                                       };
+                               }
+                               // If we have no coords (searching for a page's 
nearby), find the
+                               // page in the results and get its coords.
+                               if ( params.ggspage ) {
+                                       $.each( pages, function ( i, page ) {
+                                               if ( params.ggspage === 
page.title ) {
+                                                       loc = {
+                                                               latitude: 
page.coordinates[0].lat,
+                                                               longitude: 
page.coordinates[0].lon
+                                                       };
+                                               }
+                                       } );
+                               }
+
+                               pages = $.map( pages, function ( page, i ) {
+                                       var coords, lngLat, p;
+                                       // FIXME: API returns pageid rather 
than id, should we rename Page option ?
+                                       page.id = page.pageid;
+                                       p = new Page( page );
+                                       p.anchor = 'item_' + i;
+                                       if ( page.coordinates && loc ) { // 
FIXME: protect against bug 47133 (remove when resolved)
+                                               coords = page.coordinates[0];
+                                               lngLat = {
+                                                       latitude: coords.lat,
+                                                       longitude: coords.lon
+                                               };
+                                               // FIXME: Make part of the Page 
object
+                                               p.dist = calculateDistance( 
loc, lngLat );
+                                               p.latitude = coords.lat;
+                                               p.longitude = coords.lon;
+                                               p.proximity = 
self._distanceMessage( p.dist );
+                                       } else {
+                                               p.dist = 0;
+                                       }
+                                       if ( exclude !== page.title ) {
+                                               return p;
+                                       }
+                               } );
+
+                               pages.sort( function ( a, b ) {
+                                       return a.dist > b.dist ? 1 : -1;
+                               } );
+                               d.resolve( pages );
+                       } );
+
+                       return d;
+               }
+       };
+
+       M.define( 'mobile.nearby/NearbyApiGateway', NearbyApiGateway );
+}( mw.mobileFrontend, jQuery ) );
diff --git a/resources/mobile.notifications.overlay/NotificationsOverlay.js 
b/resources/mobile.notifications.overlay/NotificationsOverlay.js
index 2276654..145aeee 100644
--- a/resources/mobile.notifications.overlay/NotificationsOverlay.js
+++ b/resources/mobile.notifications.overlay/NotificationsOverlay.js
@@ -12,15 +12,15 @@
         */
        NotificationsOverlay = Overlay.extend( {
                className: 'overlay notifications-overlay navigation-drawer',
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        content: mw.template.get( 
'mobile.notifications.overlay', 'content.hogan' )
-               },
+               } ),
                /**
                 * @inheritdoc
                 * @cfg {Object} defaults Default options hash.
                 * @cfg {String} defaults.heading Heading text.
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        heading: mw.msg( 'notifications' ),
                        footerAnchor: new Anchor( {
                                href: mw.util.getUrl( 'Special:Notifications' ),
@@ -28,7 +28,7 @@
                                additionalClassNames: 'footer-link 
notifications-archive-link',
                                label: mw.msg( 'echo-overlay-link' )
                        } ).options
-               },
+               } ),
                /**
                 * Fall back to notifications archive page.
                 * @method
diff --git a/resources/mobile.oo/Class.js b/resources/mobile.oo/Class.js
index f21fc2e..fd11679 100644
--- a/resources/mobile.oo/Class.js
+++ b/resources/mobile.oo/Class.js
@@ -3,6 +3,18 @@
  */
 ( function ( M ) {
 
+       OO.mfExtend = function ( Child, ParentOrPrototype, prototype ) {
+               if ( prototype ) {
+                       OO.inheritClass( Child, ParentOrPrototype );
+               } else {
+                       OO.initClass( Class );
+                       prototype = ParentOrPrototype;
+               }
+               for ( key in prototype ) {
+                       Child.prototype[key] = prototype[key];
+               }
+       };
+
        /**
         * Extends a class with new methods and member properties.
         *
@@ -20,15 +32,11 @@
                function Child() {
                        return Parent.apply( this, arguments );
                }
-               OO.inheritClass( Child, Parent );
-               for ( key in prototype ) {
-                       Child.prototype[key] = prototype[key];
-               }
+               OO.mfExtend( Child, Parent, prototype );
                Child.extend = extend;
-               // FIXME: Use OOJS super here instead.
-               Child.prototype._parent = Parent.prototype;
                return Child;
        }
+       
 
        /**
         * An extensible program-code-template for creating objects
@@ -36,15 +44,22 @@
         * @class Class
         */
        function Class() {
+               OO.EventEmitter.call( this );
                this.initialize.apply( this, arguments );
        }
+       OO.mixinClass( Class, OO.EventEmitter );
+
        /**
         * Constructor, if you override it, use _super().
         * @method
         */
        Class.prototype.initialize = function () {};
        Class.extend = extend;
+       mw.log.deprecate( Class, 'extend', extend,
+               'Class is deprecated. Do not use this. Use OO.mfExtend' );
 
        M.define( 'Class', Class );
+       M.deprecate( 'Class', Class,
+               'OO.initClass, OO.inheritClass or OO.extendClass to create a 
class' );
 
 }( mw.mobileFrontend ) );
diff --git a/resources/mobile.oo/eventemitter.js 
b/resources/mobile.oo/eventemitter.js
deleted file mode 100644
index 948ee9e..0000000
--- a/resources/mobile.oo/eventemitter.js
+++ /dev/null
@@ -1,25 +0,0 @@
-( function ( M, $, OO ) {
-
-       var EventEmitter,
-               Class = M.require( 'Class' );
-
-       // HACK: wrap around oojs's EventEmitter
-       // This needs some hackery to make oojs's
-       // and MobileFrontend's different OO models get along,
-       // and we need to alias one() to once().
-       /**
-        * A base class with support for event emitting.
-        * @class EventEmitter
-        * @extends Class
-        * @uses OO.EventEmitter
-       **/
-       EventEmitter = Class.extend( $.extend( {
-               initialize: OO.EventEmitter
-       }, OO.EventEmitter.prototype ) );
-
-       M.define( 'eventemitter', EventEmitter );
-       // FIXME: if we want more of M's functionality in loaded in <head>,
-       // move this to a separate file
-       $.extend( mw.mobileFrontend, new EventEmitter() );
-
-}( mw.mobileFrontend, jQuery, OO ) );
diff --git a/resources/mobile.search/MobileWebSearchLogger.js 
b/resources/mobile.search/MobileWebSearchLogger.js
index b4cd970..0498317 100644
--- a/resources/mobile.search/MobileWebSearchLogger.js
+++ b/resources/mobile.search/MobileWebSearchLogger.js
@@ -1,7 +1,6 @@
 ( function ( M, mw, $ ) {
 
-       var Class = M.require( 'Class' ),
-               SchemaMobileWebSearch = M.require( 
'loggingSchemas/SchemaMobileWebSearch' ),
+       var SchemaMobileWebSearch = M.require( 
'loggingSchemas/SchemaMobileWebSearch' ),
                MobileWebSearchLogger;
 
        /**
@@ -10,20 +9,13 @@
         *
         * @class
         */
-       MobileWebSearchLogger = Class.extend( {
+       function MobileWebSearchLogger( schema ) {
+               this.schema = schema;
+               this.userSessionToken = null;
+               this.searchSessionToken = null;
+       }
 
-               /**
-                * @constructor
-                *
-                * @param {SchemaMobileWebSearch} schema An instance of the
-                *  SchemaMobileWebSearch class
-                */
-               initialize: function ( schema ) {
-                       this.schema = schema;
-                       this.userSessionToken = null;
-                       this.searchSessionToken = null;
-               },
-
+       MobileWebSearchLogger.prototype = {
                /**
                 * Sets the internal state required to deal with logging user 
session
                 * data.
@@ -111,7 +103,7 @@
                                timeOffsetSinceStart: timeOffsetSinceStart
                        } );
                }
-       } );
+       };
 
        /**
         * Convenience function that wires up an instance of the
diff --git a/resources/mobile.search/SearchOverlay.js 
b/resources/mobile.search/SearchOverlay.js
index f653dbf..aba9204 100644
--- a/resources/mobile.search/SearchOverlay.js
+++ b/resources/mobile.search/SearchOverlay.js
@@ -19,10 +19,10 @@
         * @uses Icon
         */
        SearchOverlay = Overlay.extend( {
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        anchor: Anchor.prototype.template,
                        icon: Icon.prototype.template
-               },
+               } ),
                className: 'overlay search-overlay',
                template: mw.template.get( 'mobile.search', 
'SearchOverlay.hogan' ),
                /**
@@ -43,7 +43,7 @@
                 * @cfg {String} defaults.action The value of wgScript
                 * @cfg {Object} defaults.feedback options for the feedback 
link below the search results
                 */
-               defaults: {
+               defaults: $.extend( {}, Overlay.prototype.defaults, {
                        clearIcon: new Icon( {
                                tagName: 'button',
                                name: 'clear',
@@ -67,7 +67,7 @@
                                } ).options,
                                prompt: mw.msg( 
'mobile-frontend-search-feedback-prompt' )
                        }
-               },
+               } ),
                /**
                 * @inheritdoc
                 */
diff --git a/resources/mobile.startup/PageApi.js 
b/resources/mobile.startup/PageApi.js
index f9f39f7..44909d8 100644
--- a/resources/mobile.startup/PageApi.js
+++ b/resources/mobile.startup/PageApi.js
@@ -87,13 +87,11 @@
         * @class PageApi
         * @extends Api
         */
-       PageApi = Api.extend( {
-               /** @inheritdoc */
-               initialize: function () {
-                       Api.prototype.initialize.apply( this, arguments );
-                       this.cache = {};
-               },
-
+       function PageApi() {
+               Api.apply( this, arguments );
+               this.cache = {};
+       }
+       OO.mfExtend( PageApi, Api, {
                /**
                 * Retrieve a page from the api
                 *
diff --git a/resources/mobile.startup/Router.js 
b/resources/mobile.startup/Router.js
index 99574ed..fad0a9a 100644
--- a/resources/mobile.startup/Router.js
+++ b/resources/mobile.startup/Router.js
@@ -1,8 +1,7 @@
 // FIXME: Merge this code with OverlayManager
 ( function ( M, $ ) {
 
-       var key, router,
-               EventEmitter = M.require( 'eventemitter' );
+       var key, router;
 
        /**
         * Does hash match entry.path?
@@ -25,11 +24,11 @@
        /**
         * Provides navigation routing and location information
         * @class Router
-        * @uses EventEmitter
+        * @mixins OO.EventEmitter
         */
        function Router() {
                var self = this;
-               EventEmitter.prototype.initialize.apply( this, arguments );
+               OO.EventEmitter.call( this );
                // use an object instead of an array for routes so that we don't
                // duplicate entries that already exist
                this.routes = {};
@@ -65,12 +64,7 @@
                        self._oldHash = self.getPath();
                } );
        }
-
-       for ( key in EventEmitter.prototype ) {
-               if ( EventEmitter.prototype.hasOwnProperty( key ) ) {
-                       Router.prototype[ key ] = EventEmitter.prototype[ key ];
-               }
-       }
+       OO.mixinClass( Router, OO.EventEmitter );
 
        /**
         * Check the current route and run appropriate callback if it matches.
diff --git a/resources/mobile.startup/Schema.js 
b/resources/mobile.startup/Schema.js
index 106981f..23686a0 100644
--- a/resources/mobile.startup/Schema.js
+++ b/resources/mobile.startup/Schema.js
@@ -1,7 +1,7 @@
 ( function ( M, $ ) {
-       var Schema,
-               Class = M.require( 'Class' ),
+       var
                settings = M.require( 'settings' ),
+               Class = M.require( 'Class' ),
                BEACON_SETTING_KEY = 'mobileFrontend/beacon';
 
        /**
@@ -55,7 +55,11 @@
         * @class Schema
         * @extends Class
         */
-       Schema = Class.extend( {
+       function Schema() {
+               this.initialize.apply( this, arguments );
+       }
+
+       OO.mfExtend( Schema, {
                /**
                 * A set of defaults to log to the schema
                 *
@@ -108,7 +112,6 @@
                                throw new Error( 'Schema needs to define a 
schema name.' );
                        }
                        this.defaults = defaults;
-                       Class.prototype.initialize.apply( this, arguments );
                },
                /**
                 * Actually log event via EventLogging
@@ -161,6 +164,10 @@
 
                deleteBeacon();
        };
+       // FIXME: Needed to give time for Gather to update
+       Schema.extend = Class.extend;
+       mw.log.deprecate( Schema, 'extend', Schema.extend,
+               'Schema.extend is deprecated. Do not use this. Use OO.mfExtend' 
);
 
        M.define( 'Schema', Schema );
 
diff --git a/resources/mobile.startup/api.js b/resources/mobile.startup/api.js
index 3c7c671..aa81593 100644
--- a/resources/mobile.startup/api.js
+++ b/resources/mobile.startup/api.js
@@ -1,14 +1,16 @@
 ( function ( M, $ ) {
        var api,
-               Api = mw.Api,
-               EventEmitter = M.require( 'eventemitter' );
-
+               Class = M.require( 'Class' );
        /**
         * JavaScript wrapper for a horrible API. Use to retrieve things.
         * @class Api
-        * @extends EventEmitter
+        * @extends mw.Api
         */
-       Api = EventEmitter.extend( mw.Api.prototype ).extend( {
+       function Api() {
+               this.initialize.apply( this, arguments );
+       }
+
+       OO.mfExtend( Api, mw.Api, {
                /**
                 * @property {String} apiUrl
                 * URL to the api endpoint (api.php)
@@ -25,11 +27,15 @@
                                options.ajax.url = this.apiUrl;
                        }
                        mw.Api.call( this, options );
-                       EventEmitter.prototype.initialize.apply( this, 
arguments );
                }
        } );
        api = new Api();
        api.Api = Api;
+       // FIXME: Here to allow Gather time to catch up.
+       Api.extend = Class.extend;
+       mw.log.deprecate( Api, 'extend', Api.extend,
+               'Api.extend is deprecated. Do not use this. Use OO.mfExtend' );
+
        M.define( 'api', api );
        M.define( 'mobile.startup/Api', Api );
 
diff --git a/resources/mobile.swipe/Swipe.js b/resources/mobile.swipe/Swipe.js
index 5577dc3..cfeadf3 100644
--- a/resources/mobile.swipe/Swipe.js
+++ b/resources/mobile.swipe/Swipe.js
@@ -1,12 +1,13 @@
 ( function ( M, $ ) {
 
-       var EventEmitter = M.require( 'eventemitter' ),
+       var Class = M.require( 'Class' ),
                Swipe;
 
        /**
         * Class to assist a view in implementing swipe gestures on a specific 
element
         *
         * @class Swipe
+        * @extends Class
         *
         * Use this class in a view to help it do things on swipe gestures.
         *
@@ -49,14 +50,14 @@
         *       } );
         *     </code>
         */
-       Swipe = EventEmitter.extend( {
+       Swipe = Class.extend( {
                /**
                 * Constructor.
                 * @param {Number} minDistance minimal distance in pixel 
between touchstart and touchend
                 * to be recognized as a swipe event. Default: 200
                 */
                initialize: function ( minDistance ) {
-                       EventEmitter.prototype.initialize.apply( this, 
arguments );
+                       Class.prototype.initialize.apply( this, arguments );
                        this.minDistance = minDistance || 200;
                },
                /**
diff --git a/resources/mobile.talk.overlays/TalkOverlay.js 
b/resources/mobile.talk.overlays/TalkOverlay.js
index e80d28d..4eccdd5 100644
--- a/resources/mobile.talk.overlays/TalkOverlay.js
+++ b/resources/mobile.talk.overlays/TalkOverlay.js
@@ -31,7 +31,7 @@
                         * generating header buttons. Default list includes an 
'add' button, which opens
                         * a new talk overlay.
                         */
-                       defaults: {
+                       defaults: $.extend( {}, Overlay.prototype.defaults, {
                                headings: undefined,
                                heading: '<strong>' + mw.msg( 
'mobile-frontend-talk-overlay-header' ) + '</strong>',
                                leadHeading: mw.msg( 
'mobile-frontend-talk-overlay-lead-header' ),
@@ -46,7 +46,7 @@
                                        additionalClassNames: 'footer-link 
talk-fullpage',
                                        label: mw.msg( 
'mobile-frontend-talk-fullpage' )
                                } ).options
-                       },
+                       } ),
 
                        /** @inheritdoc */
                        postRender: function () {
diff --git a/resources/mobile.talk.overlays/TalkSectionAddOverlay.js 
b/resources/mobile.talk.overlays/TalkSectionAddOverlay.js
index 6cec9aa..2a1031a 100644
--- a/resources/mobile.talk.overlays/TalkSectionAddOverlay.js
+++ b/resources/mobile.talk.overlays/TalkSectionAddOverlay.js
@@ -38,10 +38,10 @@
                        } ).toHtmlString()
                } ),
                template: mw.template.get( 'mobile.talk.overlays', 
'SectionAddOverlay.hogan' ),
-               templatePartials: {
+               templatePartials: $.extend( {}, 
Overlay.prototype.templatePartials, {
                        contentHeader: mw.template.get( 'mobile.talk.overlays', 
'SectionAddOverlay/contentHeader.hogan' ),
                        saveHeader: mw.template.get( 'mobile.editor.common', 
'saveHeader.hogan' )
-               },
+               } ),
                events: $.extend( {}, Overlay.prototype.events, {
                        'input .wikitext-editor, .summary': 'onTextInput',
                        'change .wikitext-editor, .summary': 'onTextInput',
diff --git a/resources/mobile.view/View.js b/resources/mobile.view/View.js
index 4529a2c..44b8973 100644
--- a/resources/mobile.view/View.js
+++ b/resources/mobile.view/View.js
@@ -1,7 +1,6 @@
 ( function ( M, $ ) {
 
-       var EventEmitter = M.require( 'eventemitter' ),
-               View,
+       var View,
                // Cached regex to split keys for `delegate`.
                delegateEventSplitter = /^(\S+)\s*(.*)$/,
                idCounter = 0;
@@ -71,7 +70,7 @@
         *     </code>
         *
         * @class View
-        * @extends EventEmitter
+        * @mixin OO.EventEmitter
         * @param {Object} options Options for the view, containing the el or
         * template data or any other information you want to use in the view.
         * Example:
@@ -86,7 +85,11 @@
         *     section.appendTo( 'body' );
         *     </pre>
         */
-       View = EventEmitter.extend( {
+       function View() {
+               this.initialize.apply( this, arguments );
+       }
+       OO.mixinClass( View, OO.EventEmitter );
+       OO.mfExtend( View, {
                /**
                 * A css class to apply to the containing element of the View.
                 * @property {String} className
@@ -158,9 +161,7 @@
                initialize: function ( options ) {
                        var self = this;
 
-                       EventEmitter.prototype.initialize.apply( this, 
arguments );
-                       this.defaults = $.extend( {}, this._parent.defaults, 
this.defaults );
-                       this.templatePartials = $.extend( {}, 
this._parent.templatePartials, this.templatePartials );
+                       OO.EventEmitter.call( this );
                        options = $.extend( {}, this.defaults, options );
                        this.options = options;
                        // Assign a unique id for dom events binding/unbinding
@@ -332,9 +333,31 @@
                        this.$el.off( eventName + '.delegateEvents' + this.cid, 
selector,
                                listener );
                }
-
        } );
 
+       /**
+        * Helper function for generating a View.
+        *
+        * @param {Object} prototype
+        * @return {View}
+        */
+       function extend( prototype ) {
+               var Child,
+                       Parent = this;
+
+               /**
+                * @ignore
+                */
+               Child = function() {
+                       OO.EventEmitter.call( this );
+                       Parent.apply( this, arguments );
+               };
+               Child.extend = extend;
+               OO.mfExtend( Child, this, prototype );
+               return Child;
+       }
+       View.extend = extend;
+
        $.each( [
                'append',
                'prepend',
diff --git a/tests/qunit/mobile.nearby/test_NearbyApiGateway.js 
b/tests/qunit/mobile.nearby/test_NearbyApiGateway.js
new file mode 100644
index 0000000..1572ee9
--- /dev/null
+++ b/tests/qunit/mobile.nearby/test_NearbyApiGateway.js
@@ -0,0 +1,113 @@
+( function ( M, $ ) {
+
+       var NearbyApi = M.require( 'modules/nearby/NearbyApi' ),
+               m;
+
+       QUnit.module( 'MobileFrontend NearbyApi', {
+               setup: function () {
+                       m = new NearbyApi();
+                       this.sandbox.stub( m, 'ajax', function () {
+                               return $.Deferred().resolve( {
+                                       query: {
+                                               pages: {
+                                                       20004112: {
+                                                               pageid: 
20004112,
+                                                               ns: 0,
+                                                               title: 'The 
Montgomery (San Francisco)',
+                                                               thumbnail: {
+                                                                       source: 
'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/The_Montgomery%2C_San_Francisco.jpg/119px-The_Montgomery%2C_San_Francisco.jpg',
+                                                                       width: 
119,
+                                                                       height: 
180
+                                                               },
+                                                               pageimage: 
'The_Montgomery,_San_Francisco.jpg',
+                                                               coordinates: [ {
+                                                                       lat: 
37.787,
+                                                                       lon: 
-122.41,
+                                                                       
primary: '',
+                                                                       globe: 
'earth'
+                                                               } ]
+                                                       },
+                                                       18618509: {
+                                                               pageid: 
18618509,
+                                                               ns: 0,
+                                                               title: 
'Wikimedia Foundation',
+                                                               thumbnail: {
+                                                                       source: 
'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c4/Wikimedia_Foundation_RGB_logo_with_text.svg/180px-Wikimedia_Foundation_RGB_logo_with_text.svg.png',
+                                                                       width: 
180,
+                                                                       height: 
180
+                                                               },
+                                                               pageimage: 
'Wikimedia_Foundation_RGB_logo_with_text.svg',
+                                                               coordinates: [ {
+                                                                       lat: 
37.787,
+                                                                       lon: 
-122.51,
+                                                                       
primary: '',
+                                                                       globe: 
'earth'
+                                                               } ]
+                                                       },
+                                                       9297443: {
+                                                               pageid: 9297443,
+                                                               ns: 0,
+                                                               title: 'W San 
Francisco',
+                                                               coordinates: [ {
+                                                                       lat: 
37.7854,
+                                                                       lon: 
-122.61,
+                                                                       
primary: '',
+                                                                       globe: 
'earth'
+                                                               } ]
+                                                       }
+                                               }
+                                       }
+                               } );
+                       } );
+               }
+       } );
+
+       QUnit.test( '#_distanceMessage', function ( assert ) {
+               var msgKm = 'mobile-frontend-nearby-distance',
+                       msgM = 'mobile-frontend-nearby-distance-meters',
+                       tests = [
+                               [ 0.4834, msgM, '490' ],
+                               [ 0.5, msgM, '500' ],
+                               [ 0.723, msgM, '730' ],
+                               [ 0.999, msgKm, '1' ],
+                               [ 1.2, msgKm, '1.20' ],
+                               [ 1.588, msgKm, '1.59' ],
+                               [ 1.123, msgKm, '1.13' ],
+                               [ 2.561, msgKm, '2.6' ],
+                               [ 10.8334, msgKm, '10.9' ]
+                       ];
+               this.sandbox.spy( mw, 'msg' );
+
+               QUnit.expect( tests.length );
+               $( tests ).each( function ( i ) {
+                       m._distanceMessage( this[ 0 ] );
+                       assert.ok( mw.msg.getCall( i ).calledWith( this[ 1 ], 
mw.language.convertNumber( this[ 2 ] ) ), 'failed test ' + i );
+               } );
+
+               mw.msg.restore();
+       } );
+
+       QUnit.test( '#getPages', 6, function ( assert ) {
+               m.getPages( {
+                       latitude: 37.786825199999996,
+                       longitude: -122.4
+               } ).done( function ( pages ) {
+                       assert.strictEqual( pages.length, 3 );
+                       assert.strictEqual( pages[ 0 ].title, 'The Montgomery 
(San Francisco)' );
+                       assert.ok( !pages[ 0 ].thumbnail.isLandscape );
+                       assert.strictEqual( pages[ 2 ].title, 'W San Francisco' 
);
+                       assert.strictEqual( pages[ 2 ].thumbnail, undefined );
+                       assert.strictEqual( pages[ 2 ].dist.toPrecision( 6 ), 
'23.3769' );
+               } );
+       } );
+
+       QUnit.test( '#getPagesAroundPage', 4, function ( assert ) {
+               m.getPagesAroundPage( 'The Montgomery (San Francisco)' ).done( 
function ( pages ) {
+                       assert.strictEqual( pages.length, 2 );
+                       assert.strictEqual( pages[ 1 ].title, 'W San Francisco' 
);
+                       assert.strictEqual( pages[ 1 ].thumbnail, undefined );
+                       assert.strictEqual( pages[ 1 ].dist.toPrecision( 6 ), 
'22.2639' );
+               } );
+       } );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/tests/qunit/mobile.oo/test_Class.js 
b/tests/qunit/mobile.oo/test_Class.js
deleted file mode 100644
index c32ad50..0000000
--- a/tests/qunit/mobile.oo/test_Class.js
+++ /dev/null
@@ -1,57 +0,0 @@
-( function ( M ) {
-       var Class = M.require( 'Class' );
-
-       QUnit.module( 'MobileFrontend Class' );
-
-       QUnit.test( '.extend', 6, function ( assert ) {
-               var Parent, Child, child;
-
-               Parent = Class.extend( {
-                       prop: 'parent',
-                       parent: function () {
-                               return 'parent';
-                       },
-                       override: function () {
-                               return 'override';
-                       },
-                       callSuper: function () {
-                               return 'super';
-                       }
-               } );
-
-               Child = Parent.extend( {
-                       prop: 'child',
-                       override: function () {
-                               return 'overriden';
-                       },
-                       child: function () {
-                               return 'child';
-                       },
-                       callSuper: function () {
-                               var _super = Parent.prototype.callSuper;
-                               return _super.apply( this ) + ' duper';
-                       }
-               } );
-
-               child = new Child();
-               assert.strictEqual( child.parent(), 'parent', 'inherit parent 
properties' );
-               assert.strictEqual( child.override(), 'overriden', 'override 
parent properties' );
-               assert.strictEqual( child.child(), 'child', 'add new 
properties' );
-               assert.strictEqual( child.callSuper(), 'super duper', 'call 
parent\'s functions' );
-               assert.strictEqual( child._parent.prop, 'parent', 'access 
parent\'s prototype through _parent' );
-               assert.strictEqual( Child.extend, Class.extend, 'make Child 
extendeable' );
-       } );
-
-       QUnit.test( '#initialize', 1, function ( assert ) {
-               var Thing, spy = this.sandbox.spy();
-
-               Thing = Class.extend( {
-                       initialize: spy
-               } );
-
-               new Thing( 'abc', 123 );
-
-               assert.ok( spy.calledWith( 'abc', 123 ), 'call #initialize when 
creating new instance' );
-       } );
-
-}( mw.mobileFrontend ) );
diff --git a/tests/qunit/mobile.oo/test_eventemitter.js 
b/tests/qunit/mobile.oo/test_eventemitter.js
deleted file mode 100644
index f6c2fe1..0000000
--- a/tests/qunit/mobile.oo/test_eventemitter.js
+++ /dev/null
@@ -1,25 +0,0 @@
-( function ( M ) {
-
-       var EventEmitter = M.require( 'eventemitter' );
-
-       QUnit.module( 'MobileFrontend EventEmitter' );
-
-       QUnit.test( '#on', 1, function ( assert ) {
-               var e = new EventEmitter(),
-                       spy = this.sandbox.spy();
-               e.on( 'testEvent', spy );
-               e.emit( 'testEvent', 'first', 2 );
-               assert.ok( spy.calledWith( 'first', 2 ), 'run callback when 
event runs' );
-       } );
-
-       QUnit.test( '#one', 2, function ( assert ) {
-               var e = new EventEmitter(),
-                       spy = this.sandbox.spy();
-               e.once( 'testEvent', spy );
-               e.emit( 'testEvent', 'first', 2 );
-               e.emit( 'testEvent', 'second', 2 );
-               assert.ok( spy.calledWith( 'first', 2 ), 'run callback when 
event runs' );
-               assert.ok( spy.calledOnce, 'run callback once' );
-       } );
-
-}( mw.mobileFrontend ) );
diff --git a/tests/qunit/mobile.overlays/test_Overlay.js 
b/tests/qunit/mobile.overlays/test_Overlay.js
index 0fac8bf..f83c83b 100644
--- a/tests/qunit/mobile.overlays/test_Overlay.js
+++ b/tests/qunit/mobile.overlays/test_Overlay.js
@@ -21,9 +21,9 @@
                var TestOverlay, overlay;
 
                TestOverlay = Overlay.extend( {
-                       templatePartials: {
+                       templatePartials: $.extend( 
Overlay.prototype.templatePartials, {
                                content: mw.template.compile( '<div 
class="content">YO</div>', 'hogan' )
-                       }
+                       } )
                } );
                overlay = new TestOverlay( {
                        heading: 'Awesome'
diff --git a/tests/qunit/mobile.startup/test_OverlayManager.js 
b/tests/qunit/mobile.startup/test_OverlayManager.js
index d519e5e..d223202 100644
--- a/tests/qunit/mobile.startup/test_OverlayManager.js
+++ b/tests/qunit/mobile.startup/test_OverlayManager.js
@@ -1,13 +1,12 @@
 ( function ( M, $ ) {
        var
                OverlayManager = M.require( 'OverlayManager' ),
-               EventEmitter = M.require( 'eventemitter' ),
                fakeRouter, overlayManager;
 
        QUnit.module( 'MobileFrontend OverlayManager', {
                setup: function () {
                        this.createFakeOverlay = function ( options ) {
-                               var fakeOverlay = new EventEmitter();
+                               var fakeOverlay = new OO.EventEmitter();
                                fakeOverlay.show = this.sandbox.spy();
                                fakeOverlay.hide = function () {
                                        this.emit( 'hide' );
@@ -18,7 +17,7 @@
                                return fakeOverlay;
                        };
 
-                       fakeRouter = new EventEmitter();
+                       fakeRouter = new OO.EventEmitter();
                        fakeRouter.getPath = this.sandbox.stub().returns( '' );
                        fakeRouter.back = this.sandbox.spy();
                        overlayManager = new OverlayManager( fakeRouter );
diff --git a/tests/qunit/mobile.startup/test_Schema.js 
b/tests/qunit/mobile.startup/test_Schema.js
index 707aa0a..bfb2552 100644
--- a/tests/qunit/mobile.startup/test_Schema.js
+++ b/tests/qunit/mobile.startup/test_Schema.js
@@ -1,9 +1,12 @@
 ( function ( $, M ) {
        var Schema = M.require( 'Schema' ),
-               TestSchema = Schema.extend( {
-                       name: 'test'
-               } );
+               TestSchema = function() {
+                       Schema.apply( this, arguments );
+               };
 
+       OO.mfExtendSchema.extend( TestSchema, {
+               name: 'test'
+       } );
        // Because these can't be undefined, we have to do this in the module
        // preamble (not setup and teardown).
        M.define( 'loggingSchemas/Schematest', TestSchema );
diff --git a/tests/qunit/mobile.view/test_View.js 
b/tests/qunit/mobile.view/test_View.js
index 5beee19..8830fa9 100644
--- a/tests/qunit/mobile.view/test_View.js
+++ b/tests/qunit/mobile.view/test_View.js
@@ -127,10 +127,10 @@
                } );
 
                ChildView = ParentView.extend( {
-                       templatePartials: {
+                       templatePartials: $.extend( 
ParentView.prototype.templatePartials, {
                                b: 3,
                                c: 4
-                       }
+                       } )
                } );
 
                view = new ChildView();
@@ -152,10 +152,10 @@
                } );
 
                ChildView = ParentView.extend( {
-                       defaults: {
+                       defaults: $.extend( ParentView.prototype.defaults, {
                                b: 3,
                                c: 4
-                       }
+                       } )
                } );
 
                view = new ChildView( {

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

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

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

Reply via email to