Mooeypoo has uploaded a new change for review. https://gerrit.wikimedia.org/r/292600
Change subject: [wip] Add a cross-wiki sidebar to the Special:Notifications page ...................................................................... [wip] Add a cross-wiki sidebar to the Special:Notifications page Change-Id: I57d827a47f80274d75364c2099a9624049a26834 --- M Resources.php M i18n/en.json M i18n/qqq.json M modules/api/mw.echo.api.APIHandler.js M modules/api/mw.echo.api.EchoApi.js M modules/controller/mw.echo.Controller.js M modules/echo.variables.less M modules/model/mw.echo.dm.FiltersModel.js A modules/model/mw.echo.dm.SourcePagesModel.js M modules/special/ext.echo.special.js A modules/styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less M modules/styles/mw.echo.ui.NotificationsInboxWidget.less A modules/styles/mw.echo.ui.PageFilterWidget.less A modules/styles/mw.echo.ui.PageNotificationsOptionWidget.less A modules/ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js M modules/ui/mw.echo.ui.DatedNotificationsWidget.js A modules/ui/mw.echo.ui.FilteredNotificationsInboxWidget.js M modules/ui/mw.echo.ui.NotificationsInboxWidget.js A modules/ui/mw.echo.ui.PageFilterWidget.js A modules/ui/mw.echo.ui.PageNotificationsOptionWidget.js 20 files changed, 650 insertions(+), 43 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Echo refs/changes/00/292600/1 diff --git a/Resources.php b/Resources.php index bca4c5c..9663fe0 100644 --- a/Resources.php +++ b/Resources.php @@ -163,6 +163,7 @@ 'scripts' => array( 'mw.echo.js', 'model/mw.echo.dm.js', + 'model/mw.echo.dm.SourcePagesModel.js', 'model/mw.echo.dm.PaginationModel.js', 'model/mw.echo.dm.FiltersModel.js', 'model/mw.echo.dm.ModelManager.js', @@ -279,6 +280,9 @@ 'ui/mw.echo.ui.DatedSubGroupListWidget.js', 'ui/mw.echo.ui.DatedNotificationsWidget.js', 'ui/mw.echo.ui.ReadStateButtonSelectWidget.js', + 'ui/mw.echo.ui.PageNotificationsOptionWidget.js', + 'ui/mw.echo.ui.PageFilterWidget.js', + 'ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js', 'ui/mw.echo.ui.NotificationsInboxWidget.js', 'special/ext.echo.special.js', ), @@ -287,6 +291,9 @@ 'styles/mw.echo.ui.DatedSubGroupListWidget.less', 'styles/mw.echo.ui.DatedNotificationsWidget.less', 'styles/mw.echo.ui.NotificationsInboxWidget.less', + 'styles/mw.echo.ui.PageNotificationsOptionWidget.less', + 'styles/mw.echo.ui.PageFilterWidget.less', + 'styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less', ), 'dependencies' => array( 'ext.echo.ui', @@ -300,6 +307,8 @@ 'echo-notification-placeholder-filters', 'echo-specialpage-pagination-numnotifications', 'echo-specialpage-pagination-range', + 'echo-specialpage-pagefilters-title', + 'echo-specialpage-pagefilters-subtitle', 'echo-more-info', 'echo-feedback', 'echo-specialpage-section-markread', diff --git a/i18n/en.json b/i18n/en.json index 1c5db68..393b24f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -99,6 +99,8 @@ "echo-specialpage-markasread-invalid-id": "Invalid event ID", "echo-specialpage-pagination-numnotifications": "$1 {{PLURAL:$1|notification|notifications}}", "echo-specialpage-pagination-range": "$1 - $2", + "echo-specialpage-pagefilters-title": "Recent activity", + "echo-specialpage-pagefilters-subtitle": "Pages with unread notifications", "notificationsmarkread-legend": "Mark notification as read", "echo-anon": "To receive notifications, [$1 create an account] or [$2 log in].", "echo-none": "You have no notifications.", diff --git a/i18n/qqq.json b/i18n/qqq.json index 9b792f1..e3bd031 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -90,6 +90,8 @@ "echo-specialpage-markasread-invalid-id": "Error message shown to users who try to mark a notification as read with an invalid event ID.", "echo-specialpage-pagination-numnotifications": "Label noting the number of notifications displayed in the page. This only appears if there is a single page of results.\n\nParameters:\n* $1 - Number of notifications in the page.", "echo-specialpage-pagination-range": "Label noting the range of the notifications displayed in the page. This only appears if there are multiple pages of results available.\n\nParameters:\n* $1 - Number of the first item.\n* $2 - Number of the last item.", + "echo-specialpage-pagefilters-title": "Title of the page filter box in Special:Notifications page.", + "echo-specialpage-pagefilters-subtitle": "Subtitle of the page filter box in Special:Notifications page.", "notificationsmarkread-legend": "Title for the form that marks a notification as read in [[Special:NotificationsMarkAsRead]]", "echo-anon": "Error message shown to users who try to visit [[Special:Notifications]] as an anon.\n\nParameters:\n* $1 - URL of signup page, with returnto pointing to Special:Notifications\n* $2 - URL of login page, with returnto pointing to Special:Notifications", "echo-none": "Message shown to users who have no notifications. Also shown in the overlay.", diff --git a/modules/api/mw.echo.api.APIHandler.js b/modules/api/mw.echo.api.APIHandler.js index dce0d6d..bd26239 100644 --- a/modules/api/mw.echo.api.APIHandler.js +++ b/modules/api/mw.echo.api.APIHandler.js @@ -57,6 +57,29 @@ mw.echo.api.APIHandler.prototype.fetchNotifications = null; /** + * Fetch all pages with unread notifications in them per wiki + * + * @param {string|string[]} [sources=*] Requested sources. If not given + * or if a '*' is given, all available sources will be queried + * @return {jQuery.Promise} Promise that is resolved with an object + * of pages with the number of unread notifications per wiki + */ + mw.echo.api.APIHandler.prototype.fetchUnreadNotificationPages = function ( sources ) { + var params = { + action: 'query', + meta: 'unreadnotificationpages' + }; + + if ( !sources || sources === '*' ) { + params.unpwikis = '*'; + } else { + sources = Array.isArray( sources ) ? sources : [ sources ]; + params.unpwikis = sources.join( '|' ); + } + + return this.api.get( params ); + }; + /** * Create a new fetchNotifications promise that queries the API and overrides * the cached promise. * diff --git a/modules/api/mw.echo.api.EchoApi.js b/modules/api/mw.echo.api.EchoApi.js index bd5ad6d..c8b2ce0 100644 --- a/modules/api/mw.echo.api.EchoApi.js +++ b/modules/api/mw.echo.api.EchoApi.js @@ -46,6 +46,20 @@ }; /** + * Fetch all pages with unread notifications in them per wiki + * + * @param {string[]} [sources=all] Requested sources + * @return {jQuery.Promise} Promise that is resolved with an object + * of pages with the number of unread notifications per wiki + */ + mw.echo.api.EchoApi.prototype.fetchUnreadNotificationPages = function ( sources ) { + return this.network.getApiHandler( 'local' ).fetchUnreadNotificationPages( sources ) + .then( function ( data ) { + return OO.getProp( data, 'query', 'unreadnotificationpages' ); + } ); + }; + + /** * Fetch notifications from the server based on type * * @param {string} type Notification type to fetch: 'alert', 'message', or 'all' diff --git a/modules/controller/mw.echo.Controller.js b/modules/controller/mw.echo.Controller.js index bfd2081..7a8befb 100644 --- a/modules/controller/mw.echo.Controller.js +++ b/modules/controller/mw.echo.Controller.js @@ -62,6 +62,34 @@ }; /** + * Fetch unread pages in all wikis and create foreign API sources + * as needed. + * + * @return {jQuery.Promise} A promise that resolves when the page filter + * model is updated with the unread notification count per page per wiki + */ + mw.echo.Controller.prototype.fetchUnreadPagesByWiki = function () { + var controller = this, + filterModel = this.manager.getFiltersModel(); + + return this.api.fetchUnreadNotificationPages() + .then( function ( data ) { + var source, + foreignSources = {}; + + // Register pages + filterModel.setSourcePageFilterData( data ); + + for ( source in data ) { + // Collect sources for API + foreignSources[ source ] = data[ source ].source; + } + + // Register the foreign sources in the API + controller.api.registerForeignSources( foreignSources ); + } ); + }; + /** * Fetch notifications from the local API and sort them by date. * This method ignores cross-wiki notifications and bundles. * diff --git a/modules/echo.variables.less b/modules/echo.variables.less index db1d2da..dcb9551 100644 --- a/modules/echo.variables.less +++ b/modules/echo.variables.less @@ -16,3 +16,11 @@ @opacity-mid: 0.8; @specialpage-separation-unit: 0.7em; +@specialpage-sidebar-width: 20em; + +@grey-light: #777; +@grey-medium: #555; +@grey-dark: #333; +@grey-darkest: #000; + +@border-color: #ccc; diff --git a/modules/model/mw.echo.dm.FiltersModel.js b/modules/model/mw.echo.dm.FiltersModel.js index 656d255..f5011ab 100644 --- a/modules/model/mw.echo.dm.FiltersModel.js +++ b/modules/model/mw.echo.dm.FiltersModel.js @@ -20,6 +20,8 @@ if ( config.readState ) { this.setReadState( config.readState ); } + + this.sourcePageModel = new mw.echo.dm.SourcePagesModel(); }; /* Initialization */ @@ -38,6 +40,58 @@ /* Methods */ /** + * Set the source page filter data + * + * @param {Object} apiObject A detailed object about sources and pages + * from the API response. + * @fires update + */ + mw.echo.dm.FiltersModel.prototype.setSourcePageFilterData = function ( apiObject ) { + this.sourcePageModel.setAllSources( apiObject ); + this.emit( 'update' ); + }; + + /** + * Get an array of all source names + * + * @return {string[]} Source names + */ + mw.echo.dm.FiltersModel.prototype.getSourcesArray = function () { + return this.sourcePageModel.getSourcesArray(); + }; + + /** + * Get a specific source's title + * + * @param {string} source Symbolic name for source + * @return {string} Source title + */ + mw.echo.dm.FiltersModel.prototype.getSourceTitle = function ( source ) { + return this.sourcePageModel.getSourceTitle( source ); + }; + + /** + * Get the total count of a source. This sums the count of all + * sub pages in that source. + * + * @param {string} source Symbolic name for source + * @return {number} Total count + */ + mw.echo.dm.FiltersModel.prototype.getSourceTotalCount = function ( source ) { + return this.sourcePageModel.getSourceTotalCount( source ); + }; + + /** + * Get a specific source's page definitions + * + * @param {string} source Symbolic name for source + * @return {Object} Page definition + */ + mw.echo.dm.FiltersModel.prototype.getSourcePages = function ( source ) { + return this.sourcePageModel.getSourcePages( source ); + }; + + /** * Set the read state filter * * @param {string} readState Notifications read state diff --git a/modules/model/mw.echo.dm.SourcePagesModel.js b/modules/model/mw.echo.dm.SourcePagesModel.js new file mode 100644 index 0000000..53337ea --- /dev/null +++ b/modules/model/mw.echo.dm.SourcePagesModel.js @@ -0,0 +1,97 @@ +( function ( mw ) { + /** + * Source pages model for notification filtering + * + * @class + * + * @constructor + * @param {Object} config Configuration object + */ + mw.echo.dm.SourcePagesModel = function MwEchoDmSourcePageModel( config ) { + config = config || {}; + + this.sources = {}; + }; + + /* Initialization */ + OO.initClass( mw.echo.dm.SourcePagesModel ); + + /** + * Set all sources and pages. This will also reset and override any + * previously set information. + * + * @param {Object} apiObject A detailed object about sources and pages + * from the API response. + */ + mw.echo.dm.SourcePagesModel.prototype.setAllSources = function ( apiObject ) { + var source; + + this.reset(); + for ( source in apiObject ) { + this.setSourcePagesDetails( source, apiObject[ source ] ); + } + }; + + /** + * Get an array of all source names + * + * @return {string[]} Array of source names + */ + mw.echo.dm.SourcePagesModel.prototype.getSourcesArray = function () { + return Object.keys( this.sources ); + }; + + mw.echo.dm.SourcePagesModel.prototype.getSourceTitle = function ( source ) { + return this.sources[ source ] && this.sources[ source ].title; + }; + + mw.echo.dm.SourcePagesModel.prototype.getSourceTotalCount = function ( source ) { + return ( this.sources[ source ] && this.sources[ source ].totalCount ) || '0'; + }; + + mw.echo.dm.SourcePagesModel.prototype.getSourcePages = function ( source ) { + return this.sources[ source ] && this.sources[ source ].pages; + }; + + /** + * Reset the data + */ + mw.echo.dm.SourcePagesModel.prototype.reset = function () { + this.sources = {}; + }; + + /** + * Set the details of a source and its page definitions + * + * @private + * @param {string} source Source symbolic name + * @param {Object} details Details object + */ + mw.echo.dm.SourcePagesModel.prototype.setSourcePagesDetails = function ( source, details ) { + var id, pageDetails, count; + + // Source information + this.sources[ source ] = { + title: details.source.title, + base: details.source.base, + totalCount: 0, + pages: {} + }; + + // Fill in pages + count = 0; + for ( id in details.pages ) { + pageDetails = details.pages[ id ]; + this.sources[ source ].pages[ pageDetails.title ] = { + title: pageDetails.title, + count: pageDetails.count, + id: id + }; + + count += parseInt( pageDetails.count ); + } + + // Update total count + this.sources[ source ].totalCount = count; + }; +} )( mediaWiki ); diff --git a/modules/special/ext.echo.special.js b/modules/special/ext.echo.special.js index e45312d..8946393 100644 --- a/modules/special/ext.echo.special.js +++ b/modules/special/ext.echo.special.js @@ -28,6 +28,8 @@ // Overlay $( 'body' ).append( mw.echo.ui.$overlay ); + controller.fetchUnreadPagesByWiki(); + $content.empty().append( specialPageContainer.$element ); } ); } )( jQuery, mediaWiki ); diff --git a/modules/styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less b/modules/styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less new file mode 100644 index 0000000..f6c6fdc --- /dev/null +++ b/modules/styles/mw.echo.ui.CrossWikiUnreadFilterWidget.less @@ -0,0 +1,16 @@ +@import '../echo.variables'; + +.mw-echo-ui-crossWikiUnreadFilterWidget { + border: 1px solid @border-color; + padding: @specialpage-separation-unit; + width: @specialpage-sidebar-width; + + &-title { + font-size: 1.3em; + font-weight: bold; + } + + &-subtitle { + color: @grey-light; + } +} diff --git a/modules/styles/mw.echo.ui.NotificationsInboxWidget.less b/modules/styles/mw.echo.ui.NotificationsInboxWidget.less index da0683e..f5388eb 100644 --- a/modules/styles/mw.echo.ui.NotificationsInboxWidget.less +++ b/modules/styles/mw.echo.ui.NotificationsInboxWidget.less @@ -1,31 +1,42 @@ @import '../echo.variables'; .mw-echo-ui-notificationsInboxWidget { - &-toolbar { - &-row { - display: table-row; - } - &-cell { + &-row { + display: table-row; + width: 100%; + } + &-cell { + display: table-cell; + vertical-align: middle; + &-placeholder { display: table-cell; - vertical-align: middle; - } - - &-top { - display: table; - margin-bottom: 3 * @specialpage-separation-unit; - - - &-placeholder { - display: table-cell; - width: 100%; - } - } - - &-bottom { - display: table; - width: inherit; - margin-left: auto; - margin-right: auto; - margin-top: 3 * @specialpage-separation-unit; + width: 100%; } } + + &-sidebar { + width: @specialpage-sidebar-width; + padding-right: 1em; + vertical-align: top; + } + + &-main { + vertical-align: top; + + &-toolbar { + &-top { + display: table; + margin-bottom: 3 * @specialpage-separation-unit; + width: 100%; + } + + &-bottom { + display: table; + width: inherit; + margin-left: auto; + margin-right: auto; + margin-top: 3 * @specialpage-separation-unit; + } + } + } + } diff --git a/modules/styles/mw.echo.ui.PageFilterWidget.less b/modules/styles/mw.echo.ui.PageFilterWidget.less new file mode 100644 index 0000000..8f22f27 --- /dev/null +++ b/modules/styles/mw.echo.ui.PageFilterWidget.less @@ -0,0 +1,8 @@ +@import '../echo.variables'; +.mw-echo-ui-pageFilterWidget { + margin-top: 2 * @specialpage-separation-unit; + + &-title { + font-weight: bold; + } +} diff --git a/modules/styles/mw.echo.ui.PageNotificationsOptionWidget.less b/modules/styles/mw.echo.ui.PageNotificationsOptionWidget.less new file mode 100644 index 0000000..15b7338 --- /dev/null +++ b/modules/styles/mw.echo.ui.PageNotificationsOptionWidget.less @@ -0,0 +1,31 @@ +@import '../echo.variables'; + +.mw-echo-ui-pageNotificationsOptionWidget { + width: 100%; + box-sizing: border-box; + + &-icon { + float: left; + .oo-ui-iconElement-icon { + display: inline-block; + } + } + + &-title { + padding: 0.2em 0; + } + + &-count { + float: right; + padding: 0.2em 0.5em; + margin-left: 0.5em; + background-color: #eee; + border-radius: 2px; + color: @grey-medium; + + .oo-ui-optionWidget-selected & { + background-color: #bbb; + } + } + +} diff --git a/modules/ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js b/modules/ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js new file mode 100644 index 0000000..77a9eff --- /dev/null +++ b/modules/ui/mw.echo.ui.CrossWikiUnreadFilterWidget.js @@ -0,0 +1,112 @@ +( function ( $, mw ) { + /** + * A filter for cross-wiki unread notifications + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {mw.echo.Controller} controller Echo controller + * @param {mw.echo.dm.ModelManager} manager Model manager + * @param {Object} [config] Configuration object + */ + mw.echo.ui.CrossWikiUnreadFilterWidget = function MwEchoUiCrossWikiUnreadFilterWidget( controller, manager, config ) { + var titleWidget, subtitleWidget; + + config = config || {}; + + // Parent + mw.echo.ui.CrossWikiUnreadFilterWidget.parent.call( this, config ); + // Mixin + OO.ui.mixin.GroupElement.call( this, config ); + + this.controller = controller; + this.manager = manager; + this.previousPageSelected = null; + + titleWidget = new OO.ui.LabelWidget( { + classes: [ 'mw-echo-ui-crossWikiUnreadFilterWidget-title' ], + label: mw.msg( 'echo-specialpage-pagefilters-title' ) + } ); + subtitleWidget = new OO.ui.LabelWidget( { + classes: [ 'mw-echo-ui-crossWikiUnreadFilterWidget-subtitle' ], + label: mw.msg( 'echo-specialpage-pagefilters-subtitle' ) + } ); + + // Events + this.manager.connect( this, { update: 'populateDataFromModel' } ); + this.aggregate( { choose: 'pageFilterChoose' } ); + this.connect( this, { pageFilterChoose: 'onPageFilterChoose' } ); + // Always have a local wiki + this.localSource = new mw.echo.ui.PageFilterWidget( + this.manager.getFiltersModel(), + mw.config.get( 'wgDBname' ) + ); + + this.$element + .addClass( 'mw-echo-ui-crossWikiUnreadFilterWidget' ) + .append( + titleWidget.$element, + subtitleWidget.$element, + this.$group + ); + }; + + /* Initialization */ + + OO.inheritClass( mw.echo.ui.CrossWikiUnreadFilterWidget, OO.ui.Widget ); + // Need to mixin base class as well + OO.mixinClass( mw.echo.ui.CrossWikiUnreadFilterWidget, OO.ui.mixin.GroupElement ); + OO.mixinClass( mw.echo.ui.CrossWikiUnreadFilterWidget, OO.ui.mixin.GroupWidget ); + + /** + * Respond to choose event in one of the page filter widgets + * + * @param {mw.echo.ui.PageFilterWidget} widget The widget the event originated from + * @param {mw.echo.ui.PageNotificationsOptionWidget} item The chosen item + * @fires filter + */ + mw.echo.ui.CrossWikiUnreadFilterWidget.prototype.onPageFilterChoose = function ( widget, item ) { + var data = item && item.getData(); + + if ( data ) { + // Unselect the previous item + if ( this.previousPageSelected ) { + this.previousPageSelected.setSelected( false ); + } + this.previousPageSelected = item; + + // Emit a choice + this.emit( 'filter', data ); + } + }; + + mw.echo.ui.CrossWikiUnreadFilterWidget.prototype.populateDataFromModel = function () { + var i, source, widget, + widgets = [], + filtersModel = this.manager.getFiltersModel(), + sources = filtersModel.getSourcesArray(); + + for ( i = 0; i < sources.length; i++ ) { + source = sources[ i ]; + widget = new mw.echo.ui.PageFilterWidget( + filtersModel, + source, + { + title: filtersModel.getSourceTitle( source ), + unreadCount: filtersModel.getSourceTotalCount( source ) + } + ); + widgets.push( widget ); + if ( source !== mw.config.get( 'wgDBname' ) ) { + this.localSource = widget; + } + } + + this.clearItems(); + this.addItems( widgets ); + // Local source is always on top + // this.addItems( [ this.localSource ], 0 ); + }; + +} )( jQuery, mediaWiki ); diff --git a/modules/ui/mw.echo.ui.DatedNotificationsWidget.js b/modules/ui/mw.echo.ui.DatedNotificationsWidget.js index b61efdd..0755cad 100644 --- a/modules/ui/mw.echo.ui.DatedNotificationsWidget.js +++ b/modules/ui/mw.echo.ui.DatedNotificationsWidget.js @@ -73,16 +73,10 @@ * Respond to model manager update event. * This event means we are repopulating the entire list and the * associated models within it. - * - * @param {Object} [models] Object of new models to populate the - * list. If not given, the method will request all models from the - * manager. */ mw.echo.ui.DatedNotificationsWidget.prototype.populateFromModel = function ( models ) { var modelId, model, subgroupWidget, groupWidgets = []; - - models = models || this.manager.getAllNotificationModels(); // Detach all attached models for ( modelId in this.models ) { diff --git a/modules/ui/mw.echo.ui.FilteredNotificationsInboxWidget.js b/modules/ui/mw.echo.ui.FilteredNotificationsInboxWidget.js new file mode 100644 index 0000000..163eb8c --- /dev/null +++ b/modules/ui/mw.echo.ui.FilteredNotificationsInboxWidget.js @@ -0,0 +1,41 @@ +( function ( $, mw ) { + /** + * A filtered inbox widget including a filters sidebar and an inbox view + * of dated notifications list. + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {mw.echo.Controller} controller Echo controller + * @param {mw.echo.dm.ModelManager} manager Model manager + * @param {Object} [config] Configuration object + * @cfg {number} [limit=25] Limit the number of notifications per page + * @cfg {jQuery} [$overlay] An overlay for the popup menus + */ + mw.echo.ui.FilteredNotificationsInboxWidget = function MwEchoUiFilteredNotificationsInboxWidget( controller, manager, config ) { + config = config || {}; + + // Parent + mw.echo.ui.FilteredNotificationsInboxWidget.parent.call( this, config ); + + this.controller = controller; + this.manager = manager; + + this.inboxWidget = new mw.echo.ui.NotificationsInboxWidget( + this.controller, + this.manager, + { + limit: config.limit, + $overlay: config.$overlay + } + ); + + this.$element + .addClass( 'mw-echo-ui-filteredNotificationsInboxWidget' ); + }; + + /* Initialization */ + + OO.inheritClass( mw.echo.ui.NotificationsInboxWidget, OO.ui.Widget ); +} )( jQuery, mediaWiki ); diff --git a/modules/ui/mw.echo.ui.NotificationsInboxWidget.js b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js index 93bded3..b4ed497 100644 --- a/modules/ui/mw.echo.ui.NotificationsInboxWidget.js +++ b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js @@ -14,6 +14,8 @@ * @cfg {jQuery} [$overlay] An overlay for the popup menus */ mw.echo.ui.NotificationsInboxWidget = function MwEchoUiNotificationsInboxWidget( controller, manager, config ) { + var $main, $sidebar; + config = config || {}; // Parent @@ -59,6 +61,12 @@ // Filter by read state this.readStateSelectWidget = new mw.echo.ui.ReadStateButtonSelectWidget(); + // Sidebar filters + this.xwikiUnreadWidget = new mw.echo.ui.CrossWikiUnreadFilterWidget( + this.controller, + this.manager + ); + // Events this.readStateSelectWidget.connect( this, { filter: 'onReadStateFilter' } ); this.manager.getFiltersModel().connect( this, { update: 'updateReadStateSelectWidget' } ); @@ -69,37 +77,41 @@ this.bottomPaginationWidget.setDisabled( true ); // Initialization - this.$element - .addClass( 'mw-echo-ui-notificationsInboxWidget' ) + $sidebar = $( '<div>' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-sidebar' ) + .append( this.xwikiUnreadWidget.$element ); + + $main = $( '<div>' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-main' ) .append( $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-top' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-top' ) .append( $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-row' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-row' ) .append( $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-readState' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-cell' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-readState' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell' ) .append( this.readStateSelectWidget.$element ), $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-top-placeholder' ), + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell-placeholder' ), $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-pagination' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-cell' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-pagination' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell' ) .append( this.topPaginationWidget.$element ) ) ), this.noticeMessageWidget.$element, this.datedListWidget.$element, $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-bottom' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-main-toolbar-bottom' ) .append( $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-row' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-row' ) .append( $( '<div>' ) - .addClass( 'mw-echo-ui-notificationsInboxWidget-toolbar-cell' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell' ) .append( this.bottomPaginationWidget.$element ) @@ -107,6 +119,19 @@ ) ); + this.$element + .addClass( 'mw-echo-ui-notificationsInboxWidget' ) + .append( + $( '<div>' ) + .addClass( 'mw-echo-ui-notificationsInboxWidget-row' ) + .append( + $sidebar + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell' ), + $main + .addClass( 'mw-echo-ui-notificationsInboxWidget-cell' ) + ) + ); + this.updateReadStateSelectWidget(); this.populateNotifications(); }; diff --git a/modules/ui/mw.echo.ui.PageFilterWidget.js b/modules/ui/mw.echo.ui.PageFilterWidget.js new file mode 100644 index 0000000..cc4d27e --- /dev/null +++ b/modules/ui/mw.echo.ui.PageFilterWidget.js @@ -0,0 +1,71 @@ +( function ( $, mw ) { + /** + * A widget that displays wikis and their pages to choose a filter + * + * @class + * @extends OO.ui.Widget + * + * @constructor + * @param {mw.echo.dm.FiltersModel} filterModel Filters model + * @param {string} source Symbolic name for the source + * @param {Object} [config] Configuration object + * @cfg {string} [title] The title of this page group, usually + * the name of the wiki that the pages belong to + * @cfg {number} [unreadCount] Number of unread notifications + */ + mw.echo.ui.PageFilterWidget = function MwEchoUiPageFilterWidget( filterModel, source, config ) { + config = config || {}; + + // Parent + mw.echo.ui.PageFilterWidget.parent.call( this, config ); + + this.model = filterModel; + this.source = source; + + // Title option + this.title = new mw.echo.ui.PageNotificationsOptionWidget( { + label: config.title, + unreadCount: config.unreadCount || this.model.getSourceTotalCount( this.source ), + data: 'all', + classes: [ 'mw-echo-ui-pageFilterWidget-title' ] + } ); + + // Initialization + this.populateDataFromModel(); + this.$element + .addClass( 'mw-echo-ui-pageFilterWidget' ); + }; + + /* Initialization */ + + OO.inheritClass( mw.echo.ui.PageFilterWidget, OO.ui.SelectWidget ); + + mw.echo.ui.PageFilterWidget.prototype.setTotalCount = function ( count ) { + this.title.setCount( count ); + }; + + mw.echo.ui.PageFilterWidget.prototype.populateDataFromModel = function () { + var page, + optionWidgets = [], + sourcePages = this.model.getSourcePages( this.source ); + + for ( page in sourcePages ) { + optionWidgets.push( + new mw.echo.ui.PageNotificationsOptionWidget( { + label: sourcePages[ page ].title, + // TODO: Pages that are a user page should + // have a user icon + icon: 'article', + unreadCount: sourcePages[ page ].count, + data: sourcePages[ page ].id, + classes: [ 'mw-echo-ui-pageFilterWidget-page' ] + } ) + ); + } + + this.clearItems(); + this.addItems( optionWidgets ); + // Title is always on top + this.addItems( [ this.title ], 0 ); + }; +} )( jQuery, mediaWiki ); diff --git a/modules/ui/mw.echo.ui.PageNotificationsOptionWidget.js b/modules/ui/mw.echo.ui.PageNotificationsOptionWidget.js new file mode 100644 index 0000000..80d0e4c --- /dev/null +++ b/modules/ui/mw.echo.ui.PageNotificationsOptionWidget.js @@ -0,0 +1,59 @@ +( function ( $, mw ) { + /** + * An option widget for the page filter in PageFilterWidget + * + * @class + * @extends OO.ui.OptionWidget + * @mixins OO.ui.mixin.IconElement + * + * @constructor + * @param {Object} [config] Configuration object + * @cfg {number} [unreadCount] Number of unread notifications + * @cfg {string} [icon] Symbolic name of an OOjs-UI icon to display + */ + mw.echo.ui.PageNotificationsOptionWidget = function MwEchoUiPageNotificationsOptionWidget( config ) { + config = config || {}; + + // Parent + mw.echo.ui.PageNotificationsOptionWidget.parent.call( this, config ); + // Mixin constructors + OO.ui.mixin.IconElement.call( this, config ); + + this.$label + .addClass( 'mw-echo-ui-pageNotificationsOptionWidget-title-label' ); + + this.unreadCountLabel = new OO.ui.LabelWidget( { + classes: [ 'mw-echo-ui-pageNotificationsOptionWidget-label-count' ], + label: String( config.unreadCount ) + } ); + + // Initialization + this.$element + .addClass( 'mw-echo-ui-pageNotificationsOptionWidget' ) + .append( + $( '<div>' ) + .addClass( 'mw-echo-ui-pageNotificationsOptionWidget-count' ) + .append( this.unreadCountLabel.$element ), + $( '<div>' ) + .addClass( 'mw-echo-ui-pageNotificationsOptionWidget-title' ) + .append( this.$label ) + ); + + if ( config.icon ) { + this.$element.prepend( + $( '<div>' ) + .addClass( 'mw-echo-ui-pageNotificationsOptionWidget-icon' ) + .append( this.$icon ) + ); + } + }; + + /* Initialization */ + + OO.inheritClass( mw.echo.ui.PageNotificationsOptionWidget, OO.ui.OptionWidget ); + OO.mixinClass( mw.echo.ui.PageNotificationsOptionWidget, OO.ui.mixin.IconElement ); + + mw.echo.ui.PageNotificationsOptionWidget.prototype.setCount = function ( count ) { + this.unreadCountLabel.setLabel( count ); + }; +} )( jQuery, mediaWiki ); -- To view, visit https://gerrit.wikimedia.org/r/292600 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I57d827a47f80274d75364c2099a9624049a26834 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Echo Gerrit-Branch: master Gerrit-Owner: Mooeypoo <mor...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits