jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/400266 )

Change subject: Add nearby suggestions
......................................................................


Add nearby suggestions

- Use GeoIP or ULSGeo cookie, to determine user's approximate position.
According to that position, provide nearby pages to user, when there
is no input query.
- Add MenuLabelWidget to enable adding labels to search results. This
allows to label two different suggestion lists: recent edits and nearby.
- Fix error handling when setting selected source page.

Bug: T111094
Change-Id: I1a6457e6e9ab14016c4e10d99a39cb4e8901d7c6
---
M extension.json
M i18n/en.json
M i18n/qqq.json
M modules/source/ext.cx.SelectedSourcePage.js
M modules/source/ext.cx.SourcePageSelector.js
M modules/source/styles/ext.cx.SourcePageSelector.less
A modules/ui/styles/widgets/mw.cx.ui.MenuLabelWidget.less
A modules/ui/widgets/mw.cx.ui.MenuLabelWidget.js
M modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
9 files changed, 208 insertions(+), 47 deletions(-)

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



diff --git a/extension.json b/extension.json
index 5d23d3b..cbc33aa 100644
--- a/extension.json
+++ b/extension.json
@@ -339,7 +339,6 @@
                        ],
                        "messages": [
                                "cx-source-page-selector-input-placeholder",
-                               "cx-source-page-selector-recent-edits-header",
                                "cx-source-page-selector-no-suggestions",
                                "cx-source-page-selector-no-search-results"
                        ]
@@ -2084,11 +2083,16 @@
                        "scripts": [
                                "ui/widgets/mw.cx.ui.PageSelectorWidget.js"
                        ],
+                       "messages": [
+                               "cx-page-selector-widget-recent-edits-label",
+                               "cx-page-selector-widget-nearby-label"
+                       ],
                        "dependencies": [
                                "ext.cx.sitemapper",
                                "jquery.uls.data",
                                "mediawiki.widgets",
                                "mw.cx.ui",
+                               "mw.cx.ui.MenuLabelWidget",
                                "mw.cx.ui.TitleOptionWidget",
                                "oojs",
                                "oojs-ui.styles.icons-interactions"
@@ -2127,6 +2131,19 @@
                                "mw.cx.ui",
                                "mediawiki.widgets"
                        ]
+               },
+               "mw.cx.ui.MenuLabelWidget": {
+                       "targets": [ "desktop", "mobile" ],
+                       "scripts": [
+                               "ui/widgets/mw.cx.ui.MenuLabelWidget.js"
+                       ],
+                       "styles": [
+                               
"ui/styles/widgets/mw.cx.ui.MenuLabelWidget.less"
+                       ],
+                       "dependencies": [
+                               "mw.cx.ui",
+                               "oojs-ui-core"
+                       ]
                }
        },
        "ResourceFileModulePaths": {
diff --git a/i18n/en.json b/i18n/en.json
index bc62fce..8493ad0 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -85,8 +85,9 @@
        "cx-tools-link-hover-tooltip": "Click to open",
        "cx-warning-unsaved-translation": "You have unsaved translations.",
        "cx-error-page-not-found": "The \"$1\" page could not be found in $2 
Wikipedia",
+       "cx-page-selector-widget-recent-edits-label": "Recently edited by you",
+       "cx-page-selector-widget-nearby-label": "Nearby",
        "cx-source-page-selector-input-placeholder": "Search for a page to 
translate",
-       "cx-source-page-selector-recent-edits-header": "Recently edited by you",
        "cx-source-page-selector-no-suggestions": "Think of any topic of your 
interest. You don’t need to be an expert to create a great translation.",
        "cx-source-page-selector-no-search-results": "No pages found for \"$1\" 
in $2",
        "cx-selected-source-page-start-translation-button": "Start translation",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 6ae6b46..60dfac5 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -94,8 +94,9 @@
        "cx-tools-link-hover-tooltip": "Tooltip text shown when the mouse is 
over the link with shift or control key is pressed.",
        "cx-warning-unsaved-translation": "Warning message shown when user 
tried to navigate away when translation is not saved.",
        "cx-error-page-not-found": "Error message shown when a page is not 
found in a given language wikipedia\n\nParameters:\n* $1 - the title of the 
page.\n* $2 - The language name",
+       "cx-page-selector-widget-recent-edits-label": "Label used in \"New 
translation\" dialog, above the suggestions based on user's recently edited 
pages.",
+       "cx-page-selector-widget-nearby-label": "Label used in \"New 
translation\" dialog, above the suggestions based on user's current location, 
showing pages in proximity to the user.",
        "cx-source-page-selector-input-placeholder": "Placeholder for the 
source page input field. Used on Content Translation page for \"New 
translation\" dialog and provides prompt to search for source page to start 
translation.",
-       "cx-source-page-selector-recent-edits-header": "Label used in \"New 
translation\" dialog, above the suggestions based on user's recently edited 
pages.",
        "cx-source-page-selector-no-suggestions": "Message used in \"New 
translation\" dialog, when there are no suggestions based on user's recently 
edited pages.",
        "cx-source-page-selector-no-search-results": "Message used in \"New 
translation\" dialog, displayed when there are no search results for user's 
input query.\n\nParameters:\n* $1 - User's input query\n* $2 - Autonym name of 
currently selected source language",
        "cx-selected-source-page-start-translation-button": "Button label 
displayed when page to be translated is selected on Content Translation 
dashboard. Clicking on it starts a new translation in 
Special:ContentTranslation.",
diff --git a/modules/source/ext.cx.SelectedSourcePage.js 
b/modules/source/ext.cx.SelectedSourcePage.js
index 0a61288..8858932 100644
--- a/modules/source/ext.cx.SelectedSourcePage.js
+++ b/modules/source/ext.cx.SelectedSourcePage.js
@@ -344,11 +344,7 @@
                        self.languageFilter.fillSourceLanguages( null, true );
                        self.languageFilter.fillTargetLanguages( null, true );
 
-                       mw.log(
-                               'Error getting page info from ' + api.apiUrl + 
' . ' +
-                               response.statusText + ' (' + response.status + 
'). ' +
-                               response.responseText
-                       );
+                       return $.Deferred().reject( 'Reason: ' + response 
).promise();
                } );
        };
 
diff --git a/modules/source/ext.cx.SourcePageSelector.js 
b/modules/source/ext.cx.SourcePageSelector.js
index dde4955..b0b0d1a 100644
--- a/modules/source/ext.cx.SourcePageSelector.js
+++ b/modules/source/ext.cx.SourcePageSelector.js
@@ -175,14 +175,10 @@
 
        SourcePageSelector.prototype.render = function () {
                var $searchResults,
-                       $recentEditsMessage,
-                       $recentEditsHeader;
+                       $recentEditsMessage;
 
                this.$container.hide(); // Starts as hidden, shown on 
this.$trigger button click
 
-               $recentEditsHeader = $( '<div>' )
-                       .addClass( 
'cx-source-page-selector__recent-edits-header' )
-                       .text( mw.msg( 
'cx-source-page-selector-recent-edits-header' ) );
                $recentEditsMessage = $( '<div>' )
                        .addClass( 
'cx-source-page-selector__no-suggestions-message' )
                        .text( mw.msg( 'cx-source-page-selector-no-suggestions' 
) );
@@ -192,7 +188,7 @@
 
                $searchResults = $( '<div>' )
                        .addClass( 'cx-source-page-selector__search-results' )
-                       .append( $recentEditsHeader, $recentEditsMessage, 
this.$noResultsMessage );
+                       .append( $recentEditsMessage, this.$noResultsMessage );
 
                this.languageFilter = new mw.cx.ui.LanguageFilter( {
                        onSourceLanguageChange: 
this.sourceLanguageChangeHandler.bind( this ),
diff --git a/modules/source/styles/ext.cx.SourcePageSelector.less 
b/modules/source/styles/ext.cx.SourcePageSelector.less
index 8e6927d..4e15b59 100644
--- a/modules/source/styles/ext.cx.SourcePageSelector.less
+++ b/modules/source/styles/ext.cx.SourcePageSelector.less
@@ -83,26 +83,25 @@
                }
 
                .cx-source-page-selector__search-message,
-               .cx-source-page-selector__no-suggestions-message,
-               .cx-source-page-selector__recent-edits-header {
+               .cx-source-page-selector__no-suggestions-message {
                        color: @colorGray7;
                        display: none;
-                       padding: 10px;
-                       font-size: 16px;
-                       font-weight: bold;
-               }
-
-               .cx-source-page-selector__search-message,
-               .cx-source-page-selector__no-suggestions-message {
                        position: absolute;
                        top: 50%;
                        left: 50%;
                        .transform( translate( -50%, -50% ) );
 
+                       .box-sizing( border-box );
                        width: 100%;
                        padding: 0 1em;
-                       font-weight: normal;
+                       font-size: 16px;
                        text-align: center;
+               }
+
+               .mw-cx-MenuLabelWidget {
+                       color: @colorGray7;
+                       font-size: 16px;
+                       font-weight: bold;
                }
 
                &.mw-cx-ui-PageSelectorWidget--no-results {
@@ -118,12 +117,6 @@
 
                &.mw-cx-ui-PageSelectorWidget--no-results:not( 
.mw-cx-ui-PageSelectorWidget--input ) {
                        .cx-source-page-selector__no-suggestions-message {
-                               display: block;
-                       }
-               }
-
-               &.mw-cx-ui-PageSelectorWidget--has-suggestions {
-                       .cx-source-page-selector__recent-edits-header {
                                display: block;
                        }
                }
diff --git a/modules/ui/styles/widgets/mw.cx.ui.MenuLabelWidget.less 
b/modules/ui/styles/widgets/mw.cx.ui.MenuLabelWidget.less
new file mode 100644
index 0000000..512a120
--- /dev/null
+++ b/modules/ui/styles/widgets/mw.cx.ui.MenuLabelWidget.less
@@ -0,0 +1,4 @@
+.mw-cx-MenuLabelWidget.oo-ui-labelElement {
+       cursor: auto;
+       padding: 10px;
+}
diff --git a/modules/ui/widgets/mw.cx.ui.MenuLabelWidget.js 
b/modules/ui/widgets/mw.cx.ui.MenuLabelWidget.js
new file mode 100644
index 0000000..25e1726
--- /dev/null
+++ b/modules/ui/widgets/mw.cx.ui.MenuLabelWidget.js
@@ -0,0 +1,38 @@
+/*!
+ *
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+
+'use strict';
+
+( function ( $, mw ) {
+
+       /**
+        * Creates an mw.cx.ui.MenuLabelWidget object.
+        *
+        * @class
+        * @extends mw.widgets.LabelWidget
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       mw.cx.ui.MenuLabelWidget = function MwCxMenuLabelWidget( config ) {
+               // Parent constructor
+               mw.cx.ui.MenuLabelWidget.parent.call( this, config );
+
+               this.$element
+                       .addClass( 'mw-cx-MenuLabelWidget' )
+                       .attr( 'role', 'label' );
+       };
+
+       /* Inheritance */
+       OO.inheritClass( mw.cx.ui.MenuLabelWidget, OO.ui.OptionWidget );
+
+       /* Static Properties */
+       mw.cx.ui.MenuLabelWidget.static.selectable = false;
+
+       mw.cx.ui.MenuLabelWidget.static.pressable = false;
+
+       mw.cx.ui.MenuLabelWidget.static.highlightable = false;
+}( jQuery, mediaWiki ) );
diff --git a/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js 
b/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
index fed4556..7485cbf 100644
--- a/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
+++ b/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
@@ -150,24 +150,88 @@
 };
 
 /**
- * @inheritdoc
+ * Get option widgets and labels from the server response.
+ * This method creates option widgets from suggested pages (when there is no 
user input) or
+ * from search results (when there is user input).
+ *
+ * @param {Object} pages Query result
+ * @return {Array} Array of OO.ui.OptionWidget menu items and 
mw.cx.ui.MenuLabelWidget labels
  */
-mw.cx.ui.PageSelectorWidget.prototype.getOptionsFromData = function ( data ) {
-       // Parent method
-       var optionsData = 
mw.cx.ui.PageSelectorWidget.super.prototype.getOptionsFromData.call( this, data 
),
+mw.cx.ui.PageSelectorWidget.prototype.getOptionsFromData = function ( pages ) {
+       var index, suggestionPage, page, optionsData, hasResults,
+               nearbyPages = pages.nearby,
+               recentEditPages = pages.recentEdits,
+               pageData = {},
+               items = [],
+               self = this;
+
+       // If there is user input, we execute parent method, process possible 
no results case and return early
+       if ( this.getQueryValue() ) {
+               optionsData = 
mw.cx.ui.PageSelectorWidget.super.prototype.getOptionsFromData.apply( this, 
arguments );
                hasResults = optionsData.length > 0;
+
+               if ( !hasResults ) {
+                       this.emit( 'noResults' );
+               }
+               this.$overlay.toggleClass( 
'mw-cx-ui-PageSelectorWidget--no-results', !hasResults );
+
+               return optionsData;
+       }
+
+       // When there is no user input, we display two lists with suggestions: 
recently edited pages and nearby pages.
+       // We need this specific override to keep the two lists separate, and 
prevent sorting by page index,
+       // which happens in parent method. Even without the sorting in parent 
method, since data is passed
+       // in objects, not arrays, the two separate lists could be mixed up, 
since ordering in JS objects
+       // is not guaranteed.
+       function processQueryResult( pages, label ) {
+               if ( !pages ) {
+                       return false;
+               }
+
+               items.push( new mw.cx.ui.MenuLabelWidget( {
+                       label: label
+               } ) );
+
+               for ( index in pages ) {
+                       suggestionPage = pages[ index ];
+
+                       pageData[ suggestionPage.title ] = {
+                               disambiguation: OO.getProp( suggestionPage, 
'pageprops', 'disambiguation' ) !== undefined,
+                               imageUrl: OO.getProp( suggestionPage, 
'thumbnail', 'source' ),
+                               description: OO.getProp( suggestionPage, 
'terms', 'description' ),
+                               originalData: suggestionPage
+                       };
+
+                       // Throw away pages from wrong namespaces. This can 
happen when 'showRedirectTargets' is true
+                       // and we encounter a cross-namespace redirect.
+                       if ( self.namespace === null || self.namespace === 
suggestionPage.ns ) {
+                               page = pageData[ suggestionPage.title ];
+                               items.push( self.createOptionWidget( 
self.getOptionWidgetData( suggestionPage.title, page ) ) );
+                       }
+               }
+
+               return true;
+       }
+
+       hasResults = processQueryResult(
+               recentEditPages,
+               mw.msg( 'cx-page-selector-widget-recent-edits-label' )
+       );
+       hasResults = processQueryResult(
+               nearbyPages,
+               mw.msg( 'cx-page-selector-widget-nearby-label' )
+       ) || hasResults;
 
        if ( !hasResults ) {
                this.emit( 'noResults' );
        }
        this.$overlay.toggleClass( 'mw-cx-ui-PageSelectorWidget--no-results', 
!hasResults );
-       // This could select the same elements as using :not() CSS selector 
with both
-       // "mw-cx-ui-PageSelectorWidget--no-results" and 
"mw-cx-ui-PageSelectorWidget--input",
-       // but the timing when those classes are added is making "Recently 
edited by you" label
-       // visible until results are fetched from the server, which can take 
long time on slow networks
-       this.$overlay.toggleClass( 
'mw-cx-ui-PageSelectorWidget--has-suggestions', !this.getQueryValue() && 
hasResults );
 
-       return optionsData;
+       if ( this.cache ) {
+               this.cache.set( pageData );
+       }
+
+       return items;
 };
 
 /**
@@ -181,22 +245,73 @@
        }
 
        this.pushPending();
-       this.getPageDetails().done( function ( data ) {
-               var pages = OO.getProp( data, 'query', 'pages' );
+       $.when(
+               this.getPageDetails(),
+               this.getNearbyPages()
+       ).done( function ( recentEdits, nearby ) {
+               var recentEditPages = OO.getProp( recentEdits, 'query', 'pages' 
),
+                       nearbyPages = OO.getProp( nearby, 'query', 'pages' );
 
                self.requestCache[ '' ] = {
-                       pages: pages || {}
+                       nearby: nearbyPages,
+                       recentEdits: recentEditPages
                };
-               self.populateLookupMenu();
        } ).fail( function ( error ) {
                mw.log( 'Error getting page data. ' + error );
        } ).always( function () {
+               self.populateLookupMenu();
                self.popPending();
        } );
 };
 
 /**
- * Get the thumbnail image, description and langlinks count for articles with 
the given titles.
+ * Get user geolocation coordinates using GeoIP or ULSGeo cookies.
+ *
+ * @return {string|null}
+ */
+mw.cx.ui.PageSelectorWidget.prototype.getUserCoordinates = function () {
+       var geoIP = mw.cookie.get( 'GeoIP', '' ), // GeoIP format: 
'FI:Helsinki:60.1756:24.9342:v4'
+               geoIPCoordsMatch = geoIP && geoIP.match( /\d+\.?\d*:\d+\.?\d*/g 
),
+               geoIPCoords = geoIPCoordsMatch && geoIPCoordsMatch[ 0 
].replace( ':', '|' ),
+               ulsGeo = JSON.parse( mw.cookie.get( 'ULSGeo' ) ), // Outside 
Wikimedia, ULS stores geolocation info in 'ULSGeo' cookie
+               ulsGeoCoords = ulsGeo && ( ulsGeo.latitude + '|' + 
ulsGeo.longitude );
+
+       return geoIPCoords || ulsGeoCoords;
+};
+
+/**
+ * Get the thumbnail image, description and langlinks count for pages 
geographically close to
+ * user's physical location.
+ *
+ * @return {jQuery.Promise}
+ */
+mw.cx.ui.PageSelectorWidget.prototype.getNearbyPages = function () {
+       var coords = this.getUserCoordinates();
+
+       if ( !coords ) {
+               // If we can't get user coordinates, use `$.when()` to create 
and return resolved promise.
+               // We return resolved promise, because we don't want `$.when` 
in populateSuggestions() method
+               // to fail if we don't have valid coordinates.
+               return $.when();
+       }
+
+       return this.siteMapper.getApi( this.language ).get( {
+               action: 'query',
+               prop: [ 'pageimages', 'pageterms', 'langlinks', 
'langlinkscount' ],
+               generator: 'geosearch',
+               piprop: 'thumbnail',
+               pithumbsize: 80,
+               lllang: this.targetLanguage,
+               wbptterms: [ 'description' ],
+               ggscoord: coords,
+               ggsradius: 1000, // Search radius in meters
+               ggslimit: 3,
+               ggsnamespace: mw.config.get( 'wgNamespaceIds' )[ '' ] // Main 
namespace
+       } ).then( function ( data ) { return data; } );
+};
+
+/**
+ * Get the thumbnail image, description and langlinks count for pages with the 
given titles.
  *
  * @return {jQuery.Promise}
  */
@@ -213,7 +328,7 @@
                        pithumbsize: 80,
                        lllang: self.targetLanguage,
                        wbptterms: [ 'description' ]
-               } );
+               } ).then( function ( data ) { return data; } );
        }, function ( error ) {
                mw.log( 'Error getting recent edit titles. ' + error );
        } );
@@ -232,7 +347,7 @@
                action: 'query',
                list: [ 'usercontribs' ],
                ucuser: userName,
-               uclimit: 5,
+               uclimit: 3,
                ucnamespace: mw.config.get( 'wgNamespaceIds' )[ '' ], // Main 
namespace
                ucprop: 'title'
        };

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I1a6457e6e9ab14016c4e10d99a39cb4e8901d7c6
Gerrit-PatchSet: 10
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Petar.petkovic <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Petar.petkovic <[email protected]>
Gerrit-Reviewer: Santhosh <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to