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