Mooeypoo has uploaded a new change for review.
https://gerrit.wikimedia.org/r/252611
Change subject: [DO NOT MERGE] Experimentally adding expandable items to
notification widgets
......................................................................
[DO NOT MERGE] Experimentally adding expandable items to notification widgets
This is an experimental branch for compraison. Another commit is under works
for the actual change. DO NOT MERGE.
Change-Id: I2bb3dabe08236625381d95f68a191e70e683af98
---
M Resources.php
M i18n/en.json
M i18n/qqq.json
M modules/demo/data/message_new.json
M modules/demo/widgets/mw.echo.demo.Page.js
M modules/nojs/mw.echo.notifications.less
A modules/ooui/mw.echo.ui.ExpandableNotificationItemWidget.js
A modules/ooui/mw.echo.ui.NotificationItemWidget.js
D modules/ooui/mw.echo.ui.NotificationOptionWidget.js
M modules/ooui/mw.echo.ui.NotificationsWidget.js
A modules/ooui/styles/mw.echo.ui.ExpandableNotificationItemWidget.less
R modules/ooui/styles/mw.echo.ui.NotificationItemWidget.less
R modules/ooui/styles/mw.echo.ui.NotificationItemWidget.modern.less
A modules/viewmodel/mw.echo.dm.ExternalAPIHandler.js
M modules/viewmodel/mw.echo.dm.NotificationItem.js
M modules/viewmodel/mw.echo.dm.NotificationsModel.js
A modules/viewmodel/mw.echo.dm.PrepopulatedNotificationsModel.js
M tests/browser/features/support/pages/article_page.rb
18 files changed, 747 insertions(+), 197 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Echo
refs/changes/11/252611/1
diff --git a/Resources.php b/Resources.php
index 15f2e56..9f32bbe 100644
--- a/Resources.php
+++ b/Resources.php
@@ -35,13 +35,15 @@
'ooui/mw.echo.ui.js',
'ooui/mw.echo.ui.PlaceholderOptionWidget.js',
'ooui/mw.echo.ui.NotificationsWidget.js',
- 'ooui/mw.echo.ui.NotificationOptionWidget.js',
+ 'ooui/mw.echo.ui.NotificationItemWidget.js',
+ 'ooui/mw.echo.ui.ExpandableNotificationItemWidget.js',
'ooui/mw.echo.ui.BadgeLinkWidget.js',
'ooui/mw.echo.ui.NotificationBadgeWidget.js'
),
'styles' => array(
'ooui/styles/mw.echo.ui.NotificationsWidget.less',
- 'ooui/styles/mw.echo.ui.NotificationOptionWidget.less',
+ 'ooui/styles/mw.echo.ui.NotificationItemWidget.less',
+
'ooui/styles/mw.echo.ui.ExpandableNotificationItemWidget.less',
'ooui/styles/mw.echo.ui.NotificationBadgeWidget.less'
),
'skinStyles' => array(
@@ -50,7 +52,7 @@
'ooui/styles/mw.echo.ui.NotificationBadgeWidget.monobook.less'
),
'modern' => array(
-
'ooui/styles/mw.echo.ui.NotificationOptionWidget.modern.less',
+
'ooui/styles/mw.echo.ui.NotificationItemWidget.modern.less',
'ooui/styles/mw.echo.ui.NotificationBadgeWidget.modern.less'
)
),
@@ -62,6 +64,12 @@
'ext.echo.logger',
'mediawiki.jqueryMsg',
'mediawiki.language',
+ // OOJS-UI icons
+ // TODO: We are only using 1-2 icons from each
+ // bundle; split them up to our own bundle so we
+ // don't load heavy icons for nothing
+ 'oojs-ui.styles.icons-user',
+ 'oojs-ui.styles.icons-alerts',
),
'messages' => array(
'echo-overlay-link',
@@ -73,6 +81,7 @@
'echo-notification-alert-text-only',
'echo-notification-message-text-only',
'echo-email-batch-bullet',
+ 'notification-link-text-expand-all',
'echo-notification-placeholder',
'tooltip-pt-notifications-alert',
'tooltip-pt-notifications-message',
@@ -87,6 +96,7 @@
'viewmodel/mw.echo.dm.NotificationItem.js',
'viewmodel/mw.echo.dm.AbstractAPIHandler.js',
'viewmodel/mw.echo.dm.APIHandler.js',
+ 'viewmodel/mw.echo.dm.ExternalAPIHandler.js',
'viewmodel/mw.echo.dm.List.js',
'viewmodel/mw.echo.dm.SortedList.js',
'viewmodel/mw.echo.dm.NotificationList.js',
@@ -94,6 +104,7 @@
),
'dependencies' => array(
'mediawiki.api',
+ 'mediawiki.ForeignApi',
'oojs'
),
'messages' => array(
diff --git a/i18n/en.json b/i18n/en.json
index 3da16a1..5cf7046 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -55,6 +55,7 @@
"echo-quotation-marks": "\"$1\"",
"echo-api-failure": "Could not retrieve notifications. Please try
again. (Error $1)",
"echo-notification-placeholder": "There are no notifications.",
+ "notification-link-text-expand-all": "Expand all",
"notification-link-text-view-message": "View message",
"notification-link-text-view-mention": "View mention",
"notification-link-text-view-changes": "View changes",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 0988bac..52b4b38 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -76,6 +76,7 @@
"echo-quotation-marks": "Unused at this time.\n\n{{optional}}\nPuts the
edit summary in quotation marks. Only translate if different than
English.\n\nParameters:\n* $1 - ...",
"echo-api-failure": "Label for the text that notes an error in
retrieving notifications for the Echo popup.\n$1 - The api error code.",
"echo-notification-placeholder": "Label for the text that appears if
there are no notifications in the Echo popup.",
+ "notification-link-text-expand-all": "Label for the button that expands
a bundled notification.\n{{Identical|View message}}",
"notification-link-text-view-message": "Label for button that links to
a message on your talk page.\n{{Identical|View message}}",
"notification-link-text-view-mention": "Label for button that links to
a discussion where you were mentioned.",
"notification-link-text-view-changes": "Label for button that links to
a \"diff\" view showing changes made to a page. This is an alternative to the
wording in {{msg-mw|notification-link-text-view-edit}}, which serves
essentially the same function.\n{{Identical|View changes}}",
diff --git a/modules/demo/data/message_new.json
b/modules/demo/data/message_new.json
index 815145f..4c23740 100644
--- a/modules/demo/data/message_new.json
+++ b/modules/demo/data/message_new.json
@@ -2,8 +2,104 @@
"query": {
"notifications": {
"message": {
- "index": [],
- "list": {}
+ "list": [
+ {
+ "id": "2958",
+ "type": "flow-post-reply",
+ "category": "flow-discussion",
+ "icon":
"/w/extensions/Flow/modules/notification/icon/Talk-ltr.png",
+ "iconType": "flow-talk",
+ "read": false,
+ "message": {
+ "header": "This is a
header",
+ "body": "This is the
notification body"
+ },
+ "timestamp": {
+ "mw": "20151122212510",
+ "raw":
"2015-11-22T21:25:10",
+ "formatted": "Today"
+ },
+ "title": {
+ "full": "Talk:Flow QA",
+ "namespace": "Talk",
+ "namespace-key": 1,
+ "text": "Flow QA"
+ },
+ "agent": {
+ "id": 0,
+ "name": "127.0.0.1"
+ },
+ "links": {
+ "primary":
"http://localhost:8080/w/index.php?title=Topic:Sra2cr2rab4qqao9&topic_showPostId=sra2d6blrk772pvt&fromnotif=1#flow-post-sra2d6blrk772pvt",
+ "secondary": [
+ {
+
"iconType": "userAvatar",
+
"label": "127.0.0.1",
+
"explicit": true,
+ "url":
"http://localhost:8080/w/index.php?title=User:127.0.0.1"
+ }
+ ]
+ }
+ },
+ {
+ "id": "2926",
+ "type": "flow-topic-renamed",
+ "category": "flow-discussion",
+ "icon":
"/w/extensions/Flow/modules/notification/icon/Talk-ltr.png",
+ "iconType": "flow-talk",
+ "read": true,
+ "message": {
+ "header": "This
notification has only header",
+ "body": ""
+ },
+ "timestamp": {
+ "mw": "20151022210626",
+ "raw":
"2015-10-22T21:06:26",
+ "formatted": "Today"
+ },
+ "title": "Talk:Flow QA",
+ "agent": {
+ "id": 0,
+ "name": "127.0.0.1"
+ },
+ "links": {
+ "primary":
"http://localhost:8080/w/index.php?title=Topic:Sra19tlcuba4huvd&fromnotif=1",
+ "secondary": [
+ {
+
"iconType": "userAvatar",
+
"label": "127.0.0.1",
+
"explicit": true,
+ "url":
"http://localhost:8080/w/index.php?title=User:127.0.0.1"
+ }
+ ]
+ }
+ },
+ {
+ "id": "4321",
+ "type": "external",
+ "icon":
"/w/extensions/Flow/modules/notification/icon/Talk-ltr.png",
+ "category": "external",
+ "apiurl":
"https://commons.wikimedia.org/w/api.php",
+ "iconType": "flow-talk",
+ "message": {
+ "header": "You have 2
notifications on Wikimedia Commons",
+ "body": ""
+ },
+ "timestamp": {
+ "mw": "20151022T210626",
+ "raw":
"2015-10-22T21:06:26",
+ "formatted": "Today"
+ },
+ "links": {
+ "primary": false,
+ "secondary": []
+ }
+ }
+ ],
+ "unread": {
+ "raw": 3,
+ "formatted": "3"
+ }
}
}
}
diff --git a/modules/demo/widgets/mw.echo.demo.Page.js
b/modules/demo/widgets/mw.echo.demo.Page.js
index 249dda2..a63b71b 100644
--- a/modules/demo/widgets/mw.echo.demo.Page.js
+++ b/modules/demo/widgets/mw.echo.demo.Page.js
@@ -103,7 +103,7 @@
this.apiContentSelectWidget.connect( this, { choose:
'onApiContentSelectWidgetChoose' } );
// Initialization
- this.loadApiFormat( config.apiFormat || 'old' );
+ this.loadApiFormat( config.apiFormat || 'new' );
this.$element
.addClass( 'mw-echo-demo-page' )
diff --git a/modules/nojs/mw.echo.notifications.less
b/modules/nojs/mw.echo.notifications.less
index 07fa1a6..34bcc59 100644
--- a/modules/nojs/mw.echo.notifications.less
+++ b/modules/nojs/mw.echo.notifications.less
@@ -1,7 +1,7 @@
-// This needs to be outside the upper selector 'NotificationOptionWidget'
+// This needs to be outside the upper selector 'notificationItemWidget'
// because the same styles also apply (for the moment, at least) to the
notification
// objects in the Special:Notifications page, which are, individually
-// not wrapped with a notificationOptionWidget.
+// not wrapped with a notificationItemWidget.
.mw-echo-state {
display: block;
padding: 15px 40px 10px 10px;
diff --git a/modules/ooui/mw.echo.ui.ExpandableNotificationItemWidget.js
b/modules/ooui/mw.echo.ui.ExpandableNotificationItemWidget.js
new file mode 100644
index 0000000..9ac9aa9
--- /dev/null
+++ b/modules/ooui/mw.echo.ui.ExpandableNotificationItemWidget.js
@@ -0,0 +1,134 @@
+( function ( mw, $ ) {
+ /**
+ * Expandable otification option widget for echo popup.
+ *
+ * @class
+ * @extends mw.echo.ui.NotificationWidget
+ * @mixin OO.ui.mixin.GroupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ * @cfg {boolean} [markReadWhenSeen=false] This option is marked as
read when it is viewed
+ */
+ mw.echo.ui.ExpandableNotificationItemWidget = function
MwEchoUiExpandableNotificationItemWidget( model, config ) {
+ var type;
+
+ config = config || {};
+
+ // Parent constructor
+ mw.echo.ui.ExpandableNotificationItemWidget.parent.call( this,
model, config );
+
+ // Mixin constructor
+ OO.ui.mixin.PendingElement.call( this, config );
+
+ type = this.model.getType();
+
+ // Get a bundleModel
+ // this.bundleModel = new mw.echo.dm.NotificationsModel(
+ // new mw.echo.dm.ExternalAPIHandler(
+ // // Foreign API url
+ // this.model.getApiUrl(),
+ // {
+ // type: $.isArray( type ) ? type.join(
'|' ) : type,
+ // limit: 5,
+ // userLang: mw.config.get(
'wgUserLanguage' ),
+ // baseParams: mw.echo.apiCallParams
+ // }
+ // ),
+ // {
+ // type: type
+ // }
+ // );
+
+ // Internal notifications widget
+ this.bundleWidget = new mw.echo.ui.NotificationsWidget(
this.model.getBundleModel(), {
+ bundle: true,
+ markReadWhenSeen: false,
+ classes: [
'mw-echo-ui-expandableNotificationItemWidget-group' ]
+ } );
+
+ this.setPendingElement( this.bundleWidget.$element );
+ this.bundleWidget.toggle( false );
+
+ // Add 'expand all' button to the actions
+ this.toggleExpandButton = new OO.ui.ButtonWidget( {
+ icon: 'speechBubbles',
+ framed: false,
+ label: mw.msg( 'notification-link-text-expand-all' ),
+ classes: [
'mw-echo-ui-notificationItemWidget-label-actions-button' ]
+ } );
+ this.$actions.prepend( this.toggleExpandButton.$element );
+
+ // Events
+ this.toggleExpandButton.connect( this, { click:
'onToggleExpandButtonClick' } );
+
+ this.$element
+ .addClass(
'mw-echo-ui-expandableNotificationItemWidget' )
+ .append( this.bundleWidget.$element );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.echo.ui.ExpandableNotificationItemWidget,
mw.echo.ui.NotificationItemWidget );
+ OO.mixinClass( mw.echo.ui.ExpandableNotificationItemWidget,
OO.ui.mixin.PendingElement );
+
+ /* Methods */
+
+ /**
+ * Respond to expand all button click
+ */
+
mw.echo.ui.ExpandableNotificationItemWidget.prototype.onToggleExpandButtonClick
= function () {
+ var widget = this;
+ // TODO: Add 'collapse' functionality
+
+ // Show the bundle
+ this.bundleWidget.toggle( true );
+
+ // Make it small while we wait for the bundle
+ this.bundleWidget.$element
+ .css( 'height', '' )
+ .addClass(
'mw-echo-ui-expandableNotificationItemWidget-group-small' );
+
+ this.populateNotifications()
+ .then( function () {
+ widget.bundleWidget.$element
+ .css( 'height',
widget.bundleWidget.$element.prop( 'scrollHeight' ) )
+ .removeClass(
'mw-echo-ui-expandableNotificationItemWidget-group-small' )
+ .addClass(
'mw-echo-ui-expandableNotificationItemWidget-group-expanded' );
+ } );
+ };
+
+ //
mw.echo.ui.ExpandableNotificationItemWidget.prototype.populateNotifications =
function () {
+ // var widget = this;
+
+ // if ( !this.bundleModel.isFetchingNotifications() ||
this.bundleModel.isFetchingErrorState() ) {
+ // this.pushPending();
+ // return this.bundleModel.fetchNotifications()
+ // .then(
+ // // Success
+ // function () {
+ // // TODO: Figure out if we
should log the impressions of remote notifications.
+ // // The current system doesn't
quite allow for cross-wiki logging capabilities
+
+ // // Display the message only if
there are no notifications
+ // if (
widget.bundleModel.isEmpty() ) {
+ //
widget.bundleWidget.resetLoadingOption( mw.msg( 'echo-notification-placeholder'
) );
+ // }
+ // },
+ // // Fail
+ // function ( errCode ) {
+ // // Display the message only if
there are no notifications
+ // if (
widget.bundleModel.isEmpty() ) {
+ //
widget.bundleWidget.resetLoadingOption( mw.msg( 'echo-api-failure', errCode ) );
+ // }
+ // }
+ // )
+ // .always( function () {
+ // widget.popPending();
+ // } );
+ // } else {
+ // return this.bundleModel.getFetchNotificationPromise();
+ // }
+ // };
+
+} )( mediaWiki, jQuery );
diff --git a/modules/ooui/mw.echo.ui.NotificationItemWidget.js
b/modules/ooui/mw.echo.ui.NotificationItemWidget.js
new file mode 100644
index 0000000..72e2b0a
--- /dev/null
+++ b/modules/ooui/mw.echo.ui.NotificationItemWidget.js
@@ -0,0 +1,213 @@
+( function ( mw, $ ) {
+ /**
+ * Notification option widget for echo popup.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ * @cfg {boolean} [markReadWhenSeen=false] This option is marked as
read when it is viewed
+ */
+ mw.echo.ui.NotificationItemWidget = function
MwEchoUiNotificationItemWidget( model, config ) {
+ var i, explicitUrls, urlObj,
+ widget = this,
+ $content = $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label-content' ),
+ $label = $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label' );
+
+ this.$actions = $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label-actions' ),
+
+ config = config || {};
+
+ this.model = model;
+
+ // Parent constructor
+ mw.echo.ui.NotificationItemWidget.parent.call( this, $.extend(
{ data: this.model.getId() }, config ) );
+
+ this.markAsReadButton = new OO.ui.ButtonWidget( {
+ icon: 'close',
+ framed: false,
+ classes: [
'mw-echo-ui-notificationItemWidget-markAsReadButton' ]
+ } );
+ // Notifications content
+ if ( this.model.getIcon() ) {
+ $label.append(
+ $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label-icon' )
+ .append( $( '<img>' ).attr( 'src',
this.model.getIcon() ) )
+ );
+ }
+
+ $content.append(
+ $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label-content-header' )
+ .append( this.model.getContentHeader() )
+ );
+ if ( this.model.getContentBody() ) {
+ $content.append(
+ $( '<div>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-label-content-body' )
+ .append( this.model.getContentBody() )
+ );
+ }
+
+ // Add the 'explicit' secondary url
+ explicitUrls = this.model.getExplicitUrls();
+ for ( i = 0; i < explicitUrls.length; i++ ) {
+ urlObj = explicitUrls[ i ];
+
+ this.$actions.append(
+ new OO.ui.ButtonWidget( {
+ icon: urlObj.iconType,
+ framed: false,
+ label: urlObj.label,
+ href: urlObj.url,
+ classes: [
'mw-echo-ui-notificationItemWidget-label-actions-button' ]
+ } ).$element
+ );
+ }
+ $content.append( this.$actions );
+ $label.append( $content );
+
+ this.toggleRead( this.model.isRead() );
+ this.toggleSeen( this.model.isSeen() );
+
+ this.markReadWhenSeen = !!config.markReadWhenSeen;
+ this.markAsReadButton.toggle( !this.markReadWhenSeen &&
!this.model.isRead() );
+
+ // Events
+ this.markAsReadButton.connect( this, { click:
'onMarkAsReadButtonClick' } );
+ this.model.connect( this, {
+ seen: 'toggleSeen',
+ read: 'toggleRead'
+ } );
+
+ this.$element
+ .addClass(
+ 'mw-echo-ui-notificationItemWidget' +
+ 'mw-echo-ui-notificationItemWidget-' +
this.model.getType()
+ )
+ .toggleClass(
+
'mw-echo-ui-notificationItemWidget-initiallyUnseen',
+ !this.model.isSeen()
+ )
+ .append(
+ this.markAsReadButton.$element,
+ $label
+ );
+
+ if ( this.model.getPrimaryUrl() ) {
+ this.$element.contents()
+ .wrapAll(
+ // HACK: Wrap the entire option with a
link that takes
+ // the user to the primary url. This is
not perfect,
+ // but it makes the behavior native to
the browser rather
+ // than us listening to click events
and opening new
+ // windows.
+ $( '<a>' )
+ .addClass(
'mw-echo-ui-notificationItemWidget-linkWrapper' )
+ .attr( 'href',
this.model.getPrimaryUrl() )
+ .on( 'click', function () {
+ // Log notification
click
+
mw.echo.logger.logInteraction(
+
mw.echo.Logger.static.actions.notificationClick,
+
mw.echo.Logger.static.context.popup,
+
widget.getModel().getId(),
+
widget.getModel().getCategory()
+ );
+ } )
+ );
+ }
+
+ // HACK: We have to remove the built-in label. When this
+ // widget is switched to a standalone widget rather than
+ // an OptionWidget we can get rid of this
+ this.$label.detach();
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.echo.ui.NotificationItemWidget, OO.ui.OptionWidget
);
+
+ /* Events */
+
+ /**
+ * @event markAsRead
+ *
+ * Mark this notification as read
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to mark as read button click
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.onMarkAsReadButtonClick =
function () {
+ this.model.toggleRead( true );
+ };
+
+ /**
+ * Reset the status of the notification without touching its
user-controlled status.
+ * For one, remove 'initiallyUnseen' which exists only for the
animation to work.
+ * This is called when new notifications are added to the parent
widget, having to
+ * reset the 'unseen' status from the old ones.
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.reset = function () {
+ this.$element.removeClass(
'mw-echo-ui-notificationItemWidget-initiallyUnseen' );
+ };
+
+ /**
+ * Toggle the read state of the widget
+ *
+ * @param {boolean} [read] The current read state. If not given, the
state will
+ * become the opposite of its current state.
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.toggleRead = function (
read ) {
+ this.read = read !== undefined ? read : !this.read;
+
+ this.$element.toggleClass(
'mw-echo-ui-notificationItemWidget-unread', !this.read );
+ this.markAsReadButton.toggle( !this.read );
+ };
+
+ /**
+ * Toggle the seen state of the widget
+ *
+ * @param {boolean} [seen] The current seen state. If not given, the
state will
+ * become the opposite of its current state.
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.toggleSeen = function (
seen ) {
+ this.seen = seen !== undefined ? seen : !this.seen;
+
+ this.$element
+ .toggleClass(
'mw-echo-ui-notificationItemWidget-unseen', !this.seen );
+ };
+
+ /**
+ * Get the notification link
+ *
+ * @return {string} Notification link
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.getModel = function () {
+ return this.model;
+ };
+
+ /**
+ * Get the notification link
+ *
+ * @return {string} Notification link
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.getPrimaryUrl = function ()
{
+ return this.model.getPrimaryUrl();
+ };
+
+ /**
+ * Disconnect events when widget is destroyed.
+ */
+ mw.echo.ui.NotificationItemWidget.prototype.destroy = function () {
+ this.model.disconnect( this );
+ };
+
+} )( mediaWiki, jQuery );
diff --git a/modules/ooui/mw.echo.ui.NotificationOptionWidget.js
b/modules/ooui/mw.echo.ui.NotificationOptionWidget.js
deleted file mode 100644
index 818d0a2..0000000
--- a/modules/ooui/mw.echo.ui.NotificationOptionWidget.js
+++ /dev/null
@@ -1,153 +0,0 @@
-( function ( mw, $ ) {
- /**
- * Notification option widget for echo popup.
- *
- * @class
- * @extends OO.ui.OptionWidget
- *
- * @constructor
- * @param {Object} [config] Configuration object
- * @cfg {boolean} [markReadWhenSeen=false] This option is marked as
read when it is viewed
- */
- mw.echo.ui.NotificationOptionWidget = function
MwEchoUiNotificationOptionWidget( model, config ) {
- var widget = this;
- config = config || {};
-
- this.model = model;
-
- // Parent constructor
- mw.echo.ui.NotificationOptionWidget.parent.call( this,
$.extend( { data: this.model.getId() }, config ) );
-
- this.markAsReadButton = new OO.ui.ButtonWidget( {
- icon: 'close',
- framed: false,
- classes: [
'mw-echo-ui-notificationOptionWidget-markAsReadButton' ]
- } );
-
- this.setLabel( this.model.getContent() );
-
- this.toggleRead( this.model.isRead() );
- this.toggleSeen( this.model.isSeen() );
-
- this.markReadWhenSeen = !!config.markReadWhenSeen;
- this.markAsReadButton.toggle( !this.markReadWhenSeen &&
!this.model.isRead() );
-
- // Events
- this.markAsReadButton.connect( this, { click:
'onMarkAsReadButtonClick' } );
- this.model.connect( this, {
- seen: 'toggleSeen',
- read: 'toggleRead'
- } );
-
- this.$element
- .addClass( 'mw-echo-ui-notificationOptionWidget
mw-echo-ui-notificationOptionWidget-' + this.model.getType() )
- .append(
- // HACK: Wrap the entire option with a link
that takes
- // the user to the primary url. This is not
perfect,
- // but it makes the behavior native to the
browser rather
- // than us listening to click events and
opening new
- // windows.
- $( '<a>' )
- .addClass(
'mw-echo-ui-notificationOptionWidget-linkWrapper' )
- .attr( 'href',
this.model.getPrimaryUrl() )
- .append(
- this.markAsReadButton.$element,
- this.$label
- )
- .on( 'click', function () {
- // Log notification click
- mw.echo.logger.logInteraction(
-
mw.echo.Logger.static.actions.notificationClick,
-
mw.echo.Logger.static.context.popup,
-
widget.getModel().getId(),
-
widget.getModel().getCategory()
- );
- } )
- );
-
- this.$element.toggleClass(
'mw-echo-ui-notificationOptionWidget-initiallyUnseen', !this.model.isSeen() );
- };
-
- /* Initialization */
-
- OO.inheritClass( mw.echo.ui.NotificationOptionWidget,
OO.ui.OptionWidget );
-
- /* Events */
-
- /**
- * @event markAsRead
- *
- * Mark this notification as read
- */
-
- /* Methods */
-
- /**
- * Respond to mark as read button click
- */
- mw.echo.ui.NotificationOptionWidget.prototype.onMarkAsReadButtonClick =
function () {
- this.model.toggleRead( true );
- };
-
- /**
- * Reset the status of the notification without touching its
user-controlled status.
- * For one, remove 'initiallyUnseen' which exists only for the
animation to work.
- * This is called when new notifications are added to the parent
widget, having to
- * reset the 'unseen' status from the old ones.
- */
- mw.echo.ui.NotificationOptionWidget.prototype.reset = function () {
- this.$element.removeClass(
'mw-echo-ui-notificationOptionWidget-initiallyUnseen' );
- };
-
- /**
- * Toggle the read state of the widget
- *
- * @param {boolean} [read] The current read state. If not given, the
state will
- * become the opposite of its current state.
- */
- mw.echo.ui.NotificationOptionWidget.prototype.toggleRead = function (
read ) {
- this.read = read !== undefined ? read : !this.read;
-
- this.$element.toggleClass(
'mw-echo-ui-notificationOptionWidget-unread', !this.read );
- this.markAsReadButton.toggle( !this.read );
- };
-
- /**
- * Toggle the seen state of the widget
- *
- * @param {boolean} [seen] The current seen state. If not given, the
state will
- * become the opposite of its current state.
- */
- mw.echo.ui.NotificationOptionWidget.prototype.toggleSeen = function (
seen ) {
- this.seen = seen !== undefined ? seen : !this.seen;
-
- this.$element
- .toggleClass(
'mw-echo-ui-notificationOptionWidget-unseen', !this.seen );
- };
-
- /**
- * Get the notification link
- *
- * @return {string} Notification link
- */
- mw.echo.ui.NotificationOptionWidget.prototype.getModel = function () {
- return this.model;
- };
-
- /**
- * Get the notification link
- *
- * @return {string} Notification link
- */
- mw.echo.ui.NotificationOptionWidget.prototype.getPrimaryUrl = function
() {
- return this.model.getPrimaryUrl();
- };
-
- /**
- * Disconnect events when widget is destroyed.
- */
- mw.echo.ui.NotificationOptionWidget.prototype.destroy = function () {
- this.model.disconnect( this );
- };
-
-} )( mediaWiki, jQuery );
diff --git a/modules/ooui/mw.echo.ui.NotificationsWidget.js
b/modules/ooui/mw.echo.ui.NotificationsWidget.js
index 10712af..6832d17 100644
--- a/modules/ooui/mw.echo.ui.NotificationsWidget.js
+++ b/modules/ooui/mw.echo.ui.NotificationsWidget.js
@@ -10,6 +10,7 @@
* @param {Object} [config] Configuration object
* @cfg {boolean} [markReadWhenSeen=false] State whether the
notifications are all
* marked as read when they are seen.
+ * @cfg {boolean} [bundle=false] This widget is a bundle widget
*/
mw.echo.ui.NotificationsWidget = function MwEchoUiNotificationsWidget(
model, config ) {
config = config || {};
@@ -17,6 +18,7 @@
this.model = model;
this.markReadWhenSeen = !!config.markReadWhenSeen;
+ this.bundle = !!config.bundle;
// Parent constructor
mw.echo.ui.NotificationsWidget.parent.call( this, config );
@@ -34,6 +36,10 @@
this.$element
.addClass( 'mw-echo-ui-notificationsWidget' );
+
+ if ( this.bundle ) {
+ this.$element.addClass(
'mw-echo-ui-notificationsWidget-bundle' );
+ }
};
/* Initialization */
@@ -49,12 +55,23 @@
* @param {number} index Index to add the item
*/
mw.echo.ui.NotificationsWidget.prototype.onModelNotificationAdd =
function ( notificationItem, index ) {
- var widget = new mw.echo.ui.NotificationOptionWidget(
- notificationItem,
- {
- markReadWhenSeen: this.markReadWhenSeen
- }
- );
+ var widget;
+
+ if ( notificationItem.getCategory() === 'external' ) {
+ widget = new
mw.echo.ui.ExpandableNotificationItemWidget(
+ notificationItem,
+ {
+ markReadWhenSeen:
this.markReadWhenSeen
+ }
+ );
+ } else {
+ widget = new mw.echo.ui.NotificationItemWidget(
+ notificationItem,
+ {
+ markReadWhenSeen:
this.markReadWhenSeen
+ }
+ );
+ }
// Fire hook for gadgets to update the option list
mw.hook( 'ext.echo.overlay.beforeShowingOverlay' ).fire(
widget.$element );
@@ -136,4 +153,8 @@
this.loadingOptionWidget.setLabel( label || '' );
this.addItems( [ this.loadingOptionWidget ] );
};
+
+ mw.echo.ui.NotificationsWidget.prototype.isBundle = function () {
+ return !!this.bundle;
+ };
} )( mediaWiki );
diff --git
a/modules/ooui/styles/mw.echo.ui.ExpandableNotificationItemWidget.less
b/modules/ooui/styles/mw.echo.ui.ExpandableNotificationItemWidget.less
new file mode 100644
index 0000000..6a88f6e
--- /dev/null
+++ b/modules/ooui/styles/mw.echo.ui.ExpandableNotificationItemWidget.less
@@ -0,0 +1,10 @@
+.mw-echo-ui-expandableNotificationItemWidget {
+ &-group {
+ overflow: hidden;
+ transition: height 500ms;
+
+ &-small {
+ height: 0.5em;
+ }
+ }
+}
diff --git a/modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.less
b/modules/ooui/styles/mw.echo.ui.NotificationItemWidget.less
similarity index 76%
rename from modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.less
rename to modules/ooui/styles/mw.echo.ui.NotificationItemWidget.less
index f1b4a5e..f3d2caa 100644
--- a/modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.less
+++ b/modules/ooui/styles/mw.echo.ui.NotificationItemWidget.less
@@ -1,6 +1,6 @@
@import '../../echo.variables';
-.mw-echo-ui-notificationOptionWidget {
+.mw-echo-ui-notificationItemWidget {
padding: 0.5em;
background-color: #F1F1F1;
border-bottom: 1px solid #DDDDDD;
@@ -10,6 +10,7 @@
&:hover > a {
text-decoration: none;
+ color: #666666;
}
&:not(:hover) a,
@@ -21,10 +22,36 @@
border-bottom: none;
}
- .oo-ui-labelElement-label {
- // We have to override this with !important because OOUI's
rules for
- // the label element are extremely strong and cannot be
overridden
- white-space: normal !important;
+ &-label {
+ white-space: normal;
+ padding: 0.5em 1em;
+ color: #666666;
+
+ &-icon {
+ display: inline-block;
+ vertical-align: top;
+ margin-right: 1em;
+ }
+
+ &-content {
+ display: inline-block;
+ &-body {
+ margin-top: 0.2em;
+ }
+
+ }
+ &-actions {
+ font-size: 0.8em;
+ margin-top: 0.5em;
+
+ &-button.oo-ui-buttonWidget .oo-ui-labelElement-label {
+ // We have to override oojs-ui's color, which
uses
+ // a very specific selector.
+ color: #666666 !important;
+ font-weight: normal !important;
+ }
+ }
+
}
&-markAsReadButton {
@@ -50,7 +77,7 @@
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
- &.mw-echo-ui-notificationOptionWidget-unread {
+ &.mw-echo-ui-notificationItemWidget-unread {
-webkit-animation-name: unseen-fadeout-to-unread;
animation-name: unseen-fadeout-to-unread;
}
diff --git
a/modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.modern.less
b/modules/ooui/styles/mw.echo.ui.NotificationItemWidget.modern.less
similarity index 91%
rename from modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.modern.less
rename to modules/ooui/styles/mw.echo.ui.NotificationItemWidget.modern.less
index 944501f..cdc5686 100644
--- a/modules/ooui/styles/mw.echo.ui.NotificationOptionWidget.modern.less
+++ b/modules/ooui/styles/mw.echo.ui.NotificationItemWidget.modern.less
@@ -1,4 +1,4 @@
-.mw-echo-ui-notificationOptionWidget {
+.mw-echo-ui-notificationItemWidget {
#p-personal & a,
#p-personal & a.new {
// Oh and double everything for :hover since Modern does that
too.
diff --git a/modules/viewmodel/mw.echo.dm.ExternalAPIHandler.js
b/modules/viewmodel/mw.echo.dm.ExternalAPIHandler.js
new file mode 100644
index 0000000..6d87818
--- /dev/null
+++ b/modules/viewmodel/mw.echo.dm.ExternalAPIHandler.js
@@ -0,0 +1,37 @@
+( function ( mw, $ ) {
+ /**
+ * External notification API handler
+ *
+ * @class
+ * @extends mw.echo.dm.APIHandler
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ */
+ mw.echo.dm.ExternalAPIHandler = function MwEchoDmExternalAPIHandler(
url, config ) {
+ config = config || {};
+
+ // Parent constructor
+ mw.echo.dm.ExternalAPIHandler.parent.call( this, config );
+
+ this.api = new mw.ForeignApi( url );
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.echo.dm.ExternalAPIHandler, mw.echo.dm.APIHandler );
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.ExternalAPIHandler.prototype.updateSeenTime = function () {
+ return $.Deferred().reject();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.ExternalAPIHandler.prototype.markAllRead = function () {
+ return $.Deferred().reject();
+ };
+} )( mediaWiki, jQuery );
diff --git a/modules/viewmodel/mw.echo.dm.NotificationItem.js
b/modules/viewmodel/mw.echo.dm.NotificationItem.js
index 0691d0e..751df80 100644
--- a/modules/viewmodel/mw.echo.dm.NotificationItem.js
+++ b/modules/viewmodel/mw.echo.dm.NotificationItem.js
@@ -8,7 +8,9 @@
* @constructor
* @param {number} id Notification id,
* @param {Object} [config] Configuration object
- * @cfg {jQuery|string} [content] The html content of this notification
+ * @cfg {Object} [apiUrl] A URL for the foreign API if this is a
bundled item
+ * @cfg {Object} [content] An object containing the 'header' and 'body'
of the
+ * notification content.
* @cfg {string} [category] The category of this notification. The
category identifies
* where the notification originates from.
* @cfg {string} [type] The notification type 'message' or 'alert'
@@ -16,6 +18,9 @@
* @cfg {boolean} [seen=false] State the seen state of the option
* @cfg {string} [timestamp] Notification timestamp in Mediawiki
timestamp format
* @cfg {string} [primaryUrl] Notification primary link in raw url
format
+ * @cfg {string} [icon] Absolute link to the notification icon
+ * @cfg {Object[]} [secondaryUrls] An object that defines this
notification's
+ * secondary links
*/
mw.echo.dm.NotificationItem = function mwFlowDmNotificationItem( id,
config ) {
var date = new Date(),
@@ -34,19 +39,22 @@
this.id = id !== undefined ? id : null;
- // TODO: We should work on the API to release and work with
actual
- // data here, rather than getting a pre-made html content of the
- // notification.
- this.content = config.content || $();
+ this.apiUrl = config.apiUrl;
+
+ // TODO: Create a submodel for bundles
+
+ this.content = $.extend( { header: '', body: '' },
config.content );
this.category = config.category || '';
this.type = config.type || 'alert';
+ this.icon = config.icon;
this.toggleRead( !!config.read );
this.toggleSeen( !!config.seen );
this.timestamp = config.timestamp || fallbackMWDate;
this.setPrimaryUrl( config.primaryUrl );
+ this.setSecondaryUrls( config.secondaryUrls );
};
/* Inheritance */
@@ -83,8 +91,16 @@
* Get NotificationItem content
* @return {jQuery|string} NotificationItem content
*/
- mw.echo.dm.NotificationItem.prototype.getContent = function () {
- return this.content;
+ mw.echo.dm.NotificationItem.prototype.getContentHeader = function () {
+ return this.content.header;
+ };
+
+ /**
+ * Get NotificationItem content
+ * @return {jQuery|string} NotificationItem content
+ */
+ mw.echo.dm.NotificationItem.prototype.getContentBody = function () {
+ return this.content.body;
};
/**
@@ -184,4 +200,51 @@
return this.primaryUrl;
};
+ /**
+ * Set the notification link
+ *
+ * @param {Object[]} links Object defining the notification secondary
links
+ */
+ mw.echo.dm.NotificationItem.prototype.setSecondaryUrls = function (
links ) {
+ this.secondaryUrls = links;
+ };
+
+ /**
+ * Get the notification secondary links
+ *
+ * @return {Object[]} Object defining the notification secondary links
+ */
+ mw.echo.dm.NotificationItem.prototype.getSecondaryUrls = function () {
+ return this.secondaryUrls;
+ };
+
+ /**
+ * Get the notification secondary explicit urls
+ *
+ * @return {Object[]} Object defining the notification secondary
explicit links
+ */
+ mw.echo.dm.NotificationItem.prototype.getExplicitUrls = function () {
+ return $.grep( this.secondaryUrls, function ( linkObj ) {
+ return linkObj && linkObj.explicit === true;
+ } );
+ };
+
+ /**
+ * Get the notification icon
+ *
+ * @return {string} Absolute link to the relevant icon
+ */
+ mw.echo.dm.NotificationItem.prototype.getIcon = function () {
+ return this.icon;
+ };
+
+ /**
+ * Get the notification API url
+ *
+ * @return {string|undefined} Foreign API url, if exists
+ */
+ mw.echo.dm.NotificationItem.prototype.getApiUrl = function () {
+ return this.apiUrl;
+ };
+
}( mediaWiki, jQuery ) );
diff --git a/modules/viewmodel/mw.echo.dm.NotificationsModel.js
b/modules/viewmodel/mw.echo.dm.NotificationsModel.js
index 4904297..693d5e8 100644
--- a/modules/viewmodel/mw.echo.dm.NotificationsModel.js
+++ b/modules/viewmodel/mw.echo.dm.NotificationsModel.js
@@ -306,7 +306,7 @@
// it exists in a failed state
return this.apiHandler.fetchNotifications( apiPromise )
.then( function ( result ) {
- var notifData, i, len, t, tlen, $content,
+ var notifData, i, t, tlen,
notificationModel, types,
optionItems = [],
idArray = [],
@@ -315,29 +315,30 @@
types = $.isArray( model.type ) ? model.type :
[ model.type ];
for ( t = 0, tlen = types.length; t < tlen; t++
) {
- data = OO.getProp( result.query,
'notifications', types[ t ] ) || { index: [] };
- for ( i = 0, len = data.index.length; i
< len; i++ ) {
- notifData = data.list[
data.index[i] ];
+ data = OO.getProp( result.query,
'notifications', types[ t ] ) || { list: [] };
+
+ for ( i = 0; i < data.list.length; i++
) {
+ notifData = data.list[ i ];
+
if ( model.getItemById(
notifData.id ) ) {
// Skip if we already
have the item
continue;
}
- // TODO: This should really be
formatted better, and the OptionWidget
- // should be the one that
displays whatever icon relates to this notification
- // according to its type.
- $content = $( $.parseHTML(
notifData['*'] ) );
notificationModel = new
mw.echo.dm.NotificationItem(
notifData.id,
{
read:
!!notifData.read,
seen:
!!notifData.read || notifData.timestamp.mw <= model.getSeenTime(),
+ type:
model.getType(),
+ apiUrl:
notifData.apiurl,
+ // TODO: Change
this to iso-860 when 'seenTime' timestamp changes
timestamp:
notifData.timestamp.mw,
category:
notifData.category,
- content:
$content,
- type:
model.getType(),
- // Hack: Get
the primary link from the $content
- primaryUrl:
$content.find( '.mw-echo-notification-primary-link' ).attr( 'href' )
+ icon:
notifData.icon,
+ content:
notifData.message,
+ primaryUrl:
OO.getProp( notifData, 'links', 'primary' ),
+ secondaryUrls:
OO.getProp( notifData, 'links', 'secondary' )
}
);
diff --git a/modules/viewmodel/mw.echo.dm.PrepopulatedNotificationsModel.js
b/modules/viewmodel/mw.echo.dm.PrepopulatedNotificationsModel.js
new file mode 100644
index 0000000..abf8492
--- /dev/null
+++ b/modules/viewmodel/mw.echo.dm.PrepopulatedNotificationsModel.js
@@ -0,0 +1,88 @@
+( function ( mw, $ ) {
+ /**
+ * Notification prepopulated view model
+ * This is a viewmodel that is assumed to either be prepopulated
already with
+ * data or is injected with data externally. The assumption is that it
does
+ * not need the API handler at all, and will handle its own items,
either
+ * preexisting or given to it.
+ *
+ * @class
+ * @extends mw.echo.dm.NotificationsModel
+ *
+ * @constructor
+ * @param {Object} [config] Configuration object
+ * @cfg {string|string[]} [type='alert'] Notification type 'alert',
'message'
+ * or an array [ 'alert', 'message' ]
+ */
+ mw.echo.dm.PrepopulatedNotificationsModel = function
MwEchoDmPrepopulatedNotificationsModel( config ) {
+ config = config || {};
+
+ mw.echo.dm.PrepopulatedNotificationsModel.parent.call( this,
null, config );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.echo.dm.PrepopulatedNotificationsModel,
mw.echo.dm.NotificationsModel );
+
+ /* Methods */
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.updateSeenTime = function () {
+ return $.Deferred().promise().reject();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.markAllRead = function () {
+ return $.Deferred().promise().reject();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.markItemReadInApi = function ()
{
+ return $.Deferred().promise().reject();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.fetchNotifications = function
() {
+ // TODO: Resolve with the ID array of the existing items
+ return $.Deferred().promise().resolve( /* idArray */ );
+ };
+
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi =
function () {
+ return $.Deferred().promise().resolve( this.getItemCount() );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.isFetchingNotifications =
function () {
+ return false;
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.echo.dm.NotificationsModel.prototype.isFetchingErrorState = function
() {
+ return false;
+ };
+
+ /**
+ * Return the fetch notifications promise
+ * @return {jQuery.Promise} Promise that is resolved when notifications
were
+ * fetched from the API.
+ */
+ mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise =
function () {
+ return $.Deferred().promise().resolve( this.getItems() );
+ };
+} )( mediaWiki, jQuery );
diff --git a/tests/browser/features/support/pages/article_page.rb
b/tests/browser/features/support/pages/article_page.rb
index 216cc62..7967dc9 100644
--- a/tests/browser/features/support/pages/article_page.rb
+++ b/tests/browser/features/support/pages/article_page.rb
@@ -26,14 +26,14 @@
end
# Notification elements
- a(:notification_option, css: '.mw-echo-ui-notificationOptionWidget')
- a(:notification_option_unread, css:
'.mw-echo-ui-notificationOptionWidget-unread')
- a(:notification_option_markRead, css:
'.mw-echo-ui-notificationOptionWidget-markAsReadButton')
+ a(:notification_option, css: '.mw-echo-ui-notificationItemWidget')
+ a(:notification_option_unread, css:
'.mw-echo-ui-notificationItemWidget-unread')
+ a(:notification_option_markRead, css:
'.mw-echo-ui-notificationItemWidget-markAsReadButton')
def num_unread_message_notifications
# Count the number of elements that are unseen notification divs
# Taken from
http://stackoverflow.com/questions/6433084/how-to-get-the-number-of-elements-having-same-attribute-in-html-in-watir
browser.elements(
- css:
'.mw-echo-ui-notificationOptionWidget-unread.mw-echo-ui-notificationOptionWidget-message'
+ css:
'.mw-echo-ui-notificationItemWidget-unread.mw-echo-ui-notificationItemWidget-message'
).size
end
end
--
To view, visit https://gerrit.wikimedia.org/r/252611
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I2bb3dabe08236625381d95f68a191e70e683af98
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Echo
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits