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

Reply via email to