Notification overlay Messages and Alerts sections

Shift to new API to support 2 tab view
When a new has no messages they will see the old style overlay with
Notifications heading. I have added tests to assure this is the case!

Later patches will:
1) Add the mark as read button only in message view
2) Note currently the tabs do not refresh when notifications is clear.
We need some kind of EventEmitter to make this sort of thing easier.

Change-Id: I62b590e81cd3fe867c4c13959cb43466aacfe2d5
This commit is contained in:
jdlrobson 2014-08-06 12:29:17 -07:00
parent 66dd838ed7
commit 9882898f92
7 changed files with 375 additions and 201 deletions

View file

@ -70,13 +70,13 @@ $wgResourceModules += array(
'mediawiki.language',
),
'messages' => array(
'echo-overlay-title',
'echo-overlay-title-overflow',
'echo-overlay-link',
'echo-none',
'echo-mark-all-as-read',
'echo-more-info',
'echo-feedback',
'echo-notification-alert',
'echo-notification-message',
'echo-email-batch-bullet'
),
),
'ext.echo.overlay.init' => $echoResourceTemplate + array(

View file

@ -94,6 +94,8 @@
"echo-email-batch-body-default": "You have a new notification.",
"echo-email-footer-default": "$2\n\nTo control which emails we send you, check your preferences:\n{{canonicalurl:{{#special:Preferences}}#mw-prefsection-echo}}\n\n$1",
"echo-email-footer-default-html": "To control which emails we send you, <a href=\"$2\" style=\"text-decoration:none; color: #3868B0;\">check your preferences</a>.<br />\n$1",
"echo-notification-alert": "Alerts",
"echo-notification-message": "Messages",
"echo-overlay-link": "All notifications",
"echo-overlay-title": "<b>Notifications</b>",
"echo-overlay-title-overflow": "<b>{{PLURAL:$1|Notifications}}</b> (showing $1 of $2 unread)",

View file

@ -112,6 +112,8 @@
"echo-email-batch-body-default": "Default message for Echo e-mail digest notifications",
"echo-email-footer-default": "Default footer content for Echo text e-mail notifications. Parameters:\n* $1 - the address of the organization that sent the email\n* $2 - \"-------...\" ({{msg-mw|echo-email-batch-separator}})\n\nFor HTML version, see {{msg-mw|echo-email-footer-default-html}}.",
"echo-email-footer-default-html": "Default footer content for Echo html e-mail notifications. Parameters:\n* $1 - the address of the organization that sent the email\n* $2 - the URL to the notification preference page\nFor plain-text version, see {{msg-mw|Echo-email-footer-default}}.",
"echo-notification-alert": "Label for alert notifications tab in Echo overlay.",
"echo-notification-message": "Label for message notifications tab in Echo overlay.",
"echo-overlay-link": "Link to \"all notifications\" at the bottom of the overlay.\n{{Identical|All notifications}}",
"echo-overlay-title": "Title at the top of the notifications overlay. Should include bold tags.",
"echo-overlay-title-overflow": "Title at the top of the notifications overlay when there are additional unread notifications that are not being shown.\n\nParameters:\n* $1 - the number of unread notifications being shown\n* $2 - the total number of unread notifications that exist",

View file

@ -1,4 +1,4 @@
( function ( mw ) {
( function ( mw, $ ) {
'use strict';
mw.echo = {
@ -61,6 +61,13 @@
mw.loader.using( 'ext.eventLogging', function() {
mw.eventLog.logEvent( 'EchoInteraction', myEvt );
} );
},
/**
* @method
* @return jQuery element corresponding to the badge reflecting the notification count
*/
getBadge: function() {
return $( '.mw-echo-notifications-badge' );
}
};
@ -74,4 +81,4 @@
} );
} );
}
} )( mediaWiki );
} )( mediaWiki, jQuery );

View file

@ -7,145 +7,56 @@
function EchoOverlay( apiResultNotifications ) {
this.api = mw.echo.overlay.api;
// set internal properties
this.tabs = [];
this._buildOverlay( apiResultNotifications );
}
EchoOverlay.prototype = {
/**
* @var array a list of unread notification ids that are visible in the UI
*/
function EchoOverlayTab( name, notifications ) {
this.api = mw.echo.overlay.api;
this.name = name;
this.unread = [];
this._buildList( notifications );
}
EchoOverlayTab.prototype = {
unread: [],
/**
* @var object current count status of notification types
*/
notificationCount: {
/* @var integer length of all notifications (both unread and read) that will be visible in the overlay */
all: 0,
/* @var string a string representation the current number of unread notifications (1, 99, 99+) */
unread: '0',
/* @var integer the total number of all unread notifications including those not in the overlay */
unreadRaw: 0
getUnreadIds: function() {
return this.unread;
},
/**
* @param newCount formatted count
* @param rawCount unformatted count
* Mark all unread notifications as read
* @method
* @return jQuery.Deferred
*/
updateCount: function ( newCount, rawCount ) {
var $badge = $( '.mw-echo-notifications-badge' );
$badge.text( newCount );
if ( rawCount !== '0' && rawCount !== 0 ) {
$badge.addClass( 'mw-echo-unread-notifications' );
markAsRead: function() {
var self = this;
// only need to mark as read if there is unread item
if ( this.unread.length ) {
return this.api.post( mw.echo.desktop.appendUseLang( {
'action' : 'echomarkread',
'list' : this.unread.join( '|' ),
'token': mw.user.tokens.get( 'editToken' )
} ) ).then( function ( result ) {
return result.query.echomarkread[self.name];
} );
} else {
$badge.removeClass( 'mw-echo-unread-notifications' );
return new $.Deferred();
}
this.notificationCount.unread = newCount;
this.notificationCount.unreadRaw = rawCount;
},
configuration: mw.config.get( 'wgEchoOverlayConfiguration' ),
_getFooterElement: function() {
var $prefLink = $( '#pt-preferences a' ),
$overlayFooter = $( '<div>' )
.attr( 'id', 'mw-echo-overlay-footer' );
// add link to notifications archive
$overlayFooter.append(
$( '<a>' )
.attr( 'id', 'mw-echo-overlay-link' )
.addClass( 'mw-echo-grey-link' )
.attr( 'href', getUrl( 'Special:Notifications' ) )
.text( mw.msg( 'echo-overlay-link' ) )
.click( function () {
mw.echo.logInteraction( 'ui-archive-link-click', 'flyout' );
} )
.hover(
function() {
$( this ).removeClass( 'mw-echo-grey-link' );
},
function() {
$( this ).addClass( 'mw-echo-grey-link' );
}
)
);
// add link to notification preferences
$overlayFooter.append(
$( '<a>' )
.html( $prefLink.html() )
.attr( 'id', 'mw-echo-overlay-pref-link' )
.addClass( 'mw-echo-grey-link' )
.attr( 'href', $prefLink.attr( 'href' ) + '#mw-prefsection-echo' )
.click( function () {
mw.echo.logInteraction( 'ui-prefs-click', 'flyout' );
} )
.hover(
function() {
$( this ).removeClass( 'mw-echo-grey-link' );
},
function() {
$( this ).addClass( 'mw-echo-grey-link' );
}
)
);
return $overlayFooter;
},
_getTitleElement: function() {
var titleText,
counter = this.notificationCount,
notificationsCount = counter.all,
unreadRawTotalCount = counter.unreadRaw,
unreadTotalCount = counter.unread,
unreadCount = this.unread.length,
$title = $( '<div>' ).addClass( 'mw-echo-overlay-title' );
if ( notificationsCount > 0 ) {
if ( unreadRawTotalCount > unreadCount ) {
titleText = mw.msg(
'echo-overlay-title-overflow',
mw.language.convertNumber( unreadCount ),
mw.language.convertNumber( unreadTotalCount )
);
} else {
titleText = mw.msg( 'echo-overlay-title' );
}
} else {
titleText = mw.msg( 'echo-none' );
}
// Add the header to the title area
$( '<div>' )
.attr( 'id', 'mw-echo-overlay-title-text' )
.html( titleText )
.appendTo( $title );
// Add help button
$( '<a>' )
.attr( 'href', mw.config.get( 'wgEchoHelpPage' ) )
.attr( 'title', mw.msg( 'echo-more-info' ) )
.attr( 'id', 'mw-echo-overlay-moreinfo-link' )
.attr( 'target', '_blank' )
.click( function () {
mw.echo.logInteraction( 'ui-help-click', 'flyout' );
} )
.appendTo( $title );
return $title;
},
/**
* Builds an Echo notifications list
* @method
* @param object as returned by the api of notification items
* @param string tabName the tab
* @param object notifications as returned by the api of notification items
* @return jQuery element
*/
_buildNotificationList: function( notifications ) {
_buildList: function( notifications ) {
var self = this,
$ul = $( '<ul>' ).addClass( 'mw-echo-notifications' )
.data( 'tab', this )
.css( 'max-height', $( window ).height() - 134 );
$.each( notifications.index, function ( index, id ) {
var $wrapper,
data = notifications.list[id],
@ -209,52 +120,178 @@
mw.echo.setUpDismissability( $li );
}
} );
this.$el = $ul;
}
};
EchoOverlay.prototype = {
/**
* @var string the name of the tab that is currently active
*/
activeTabName: 'alert',
/**
* @var array a list of EchoOverlayTabs
*/
tabs: [],
/**
* @var object current count status of notification types
*/
notificationCount: {
/* @var integer length of all notifications (both unread and read) that will be visible in the overlay */
all: 0,
/* @var string a string representation the current number of unread notifications (1, 99, 99+) */
unread: '0',
/* @var integer the total number of all unread notifications including those not in the overlay */
unreadRaw: 0
},
/**
* FIXME: This should be pulled out of EchoOverlay and use an EventEmitter.
* @param newCount formatted count
* @param rawCount unformatted count
*/
updateBadgeCount: function ( newCount, rawCount ) {
var $badge = mw.echo.getBadge();
$badge.text( newCount );
if ( rawCount !== '0' && rawCount !== 0 ) {
$badge.addClass( 'mw-echo-unread-notifications' );
} else {
$badge.removeClass( 'mw-echo-unread-notifications' );
}
this.notificationCount.unread = newCount;
this.notificationCount.unreadRaw = rawCount;
},
configuration: mw.config.get( 'wgEchoOverlayConfiguration' ),
_getFooterElement: function() {
var $prefLink = $( '#pt-preferences a' ),
$overlayFooter = $( '<div>' )
.attr( 'id', 'mw-echo-overlay-footer' );
// add link to notifications archive
$overlayFooter.append(
$( '<a>' )
.attr( 'id', 'mw-echo-overlay-link' )
.addClass( 'mw-echo-grey-link' )
.attr( 'href', getUrl( 'Special:Notifications' ) )
.text( mw.msg( 'echo-overlay-link' ) )
.click( function () {
mw.echo.logInteraction( 'ui-archive-link-click', 'flyout' );
} )
.hover(
function() {
$( this ).removeClass( 'mw-echo-grey-link' );
},
function() {
$( this ).addClass( 'mw-echo-grey-link' );
}
)
);
// add link to notification preferences
$overlayFooter.append(
$( '<a>' )
.html( $prefLink.html() )
.attr( 'id', 'mw-echo-overlay-pref-link' )
.addClass( 'mw-echo-grey-link' )
.attr( 'href', $prefLink.attr( 'href' ) + '#mw-prefsection-echo' )
.click( function () {
mw.echo.logInteraction( 'ui-prefs-click', 'flyout' );
} )
.hover(
function() {
$( this ).removeClass( 'mw-echo-grey-link' );
},
function() {
$( this ).addClass( 'mw-echo-grey-link' );
}
)
);
return $overlayFooter;
},
_showTabList: function( tab ) {
var $lists = this.$el.find( '.mw-echo-notifications' ).hide(),
self = this;
this.activeTabName = tab.name;
$lists.each( function() {
if ( $( this ).data( 'tab' ).name === tab.name ) {
$( this ).show();
tab.markAsRead().done( function( data ) {
self.updateBadgeCount( data.count, data.rawcount );
} );
}
} );
},
_getTabsElement: function() {
var $li,
$ul = $( '<ul>' ), self = this;
$.each( this.tabs, function( i, echoTab ) {
var
tabName = echoTab.name,
// @todo: Pass the number of unread messages
label = mw.msg( 'echo-notification-' + tabName );
$li = $( '<li>' )
.on( 'click', function() {
var $this = $( this );
$ul.find( 'li' ).removeClass( 'mw-echo-section-current' );
$this.addClass( 'mw-echo-section-current' );
self._showTabList( $this.data( 'tab' ) );
} )
.data( 'tab', echoTab )
.addClass( i === 0 ? 'mw-echo-section-current' : '' )
.text( label )
.appendTo( $ul );
} );
return $ul;
},
getUnreadCount: function() {
var count = 0;
$.each( this.tabs, function( i, tab ) {
count += tab.getUnreadIds().length;
} );
return count;
},
_getTitleElement: function() {
var tabs = this._getTabsElement(),
$title = $( '<div>' ).addClass( 'mw-echo-overlay-title' );
$title.append( tabs );
this._showTabList( this.tabs[0] );
return $title;
},
_buildOverlay: function ( notifications ) {
var $ul,
$overlay = $( '<div>' ).addClass( 'mw-echo-overlay' ),
self = this;
this.notificationCount.all = notifications.index.length;
if ( this.notificationCount.all !== undefined ) {
self.updateCount( notifications.count, notifications.rawcount );
}
$ul = self._buildNotificationList( notifications );
self._getTitleElement().
appendTo( $overlay );
if ( $ul.find( 'li' ).length ) {
$ul.appendTo( $overlay );
}
$overlay.append( self._getFooterElement() );
self.markAsRead();
var tabs,
self = this,
$overlay = $( '<div>' ).addClass( 'mw-echo-overlay' );
this.$el = $overlay;
},
/**
* Mark a list of notifications as read
* @method
*/
markAsRead: function() {
var self = this;
// only need to mark as read if there is unread item
if ( this.unread.length ) {
this.api.post( mw.echo.desktop.appendUseLang( {
'action' : 'echomarkread',
'list' : this.unread.join( '|' ),
'token': mw.user.tokens.get( 'editToken' )
} ) ).done( function ( result ) {
var count;
if ( result.query.echomarkread.count !== undefined ) {
count = result.query.echomarkread.count;
self.updateCount( count, result.query.echomarkread.rawcount );
}
} );
if ( notifications.message.index.length ) {
tabs = [ 'message', 'alert' ];
} else {
tabs = [ 'alert' ];
}
$.each( tabs, function( i, tabName ) {
var tab = new EchoOverlayTab( tabName, notifications[tabName] );
self.$el.append( tab.$el );
self.tabs.push( tab );
self.notificationCount.all += notifications[tabName].index.length;
} );
$overlay.prepend( this._getTitleElement() );
$overlay.append( this._getFooterElement() );
}
};
@ -276,6 +313,8 @@
var apiData = {
'action' : 'query',
'meta' : 'notifications',
notsections : 'alert|message',
notgroupbysection: 1,
'notformat' : 'flyout',
'notlimit' : this.notificationLimit,
'notprop' : 'index|list|count'

View file

@ -25,6 +25,7 @@
#p-personal {
.mw-echo-overlay-title ul,
ul.mw-echo-notifications {
// override #p-personal ul in core
padding: 0;
@ -43,6 +44,10 @@
}
.mw-echo-overlay {
.mw-echo-section-current {
font-weight: bold;
}
a.mw-echo-grey-link {
color: #6D6D6D;
}
@ -106,6 +111,24 @@
border-bottom: 1px solid #DDDDDD;
}
/* override #p-personal li*/
#p-personal .mw-echo-overlay-title li {
font-size: 1em;
margin-left: 0;
}
.mw-echo-overlay-title li {
&::after {
content: " · ";
padding: 0 .25em;
}
&:last-child {
&::after {
content: '';
}
}
}
#mw-echo-overlay-title-text {
display: inline;
zoom: 1;

View file

@ -1,67 +1,168 @@
( function( $, mw ) {
QUnit.module( 'ext.echo.overlay', {
setup: function() {
var ApiStub = function() {};
this.$badge = $( '<a class="mw-echo-notifications-badge mw-echo-unread-notifications">1</a>' );
this.sandbox.stub( mw.echo, 'getBadge' ).returns( this.$badge );
// Kill any existing overlays to avoid clashing with other tests
$( '.mw-echo-overlay' ).remove();
var ApiStub = function( mode ) {
this.mode = mode;
};
ApiStub.prototype = {
post: function() {
return $.Deferred().resolve( {
query: {
echomarkread: {
count: 0
}
}
} );
post: function( data ) {
return new $.Deferred().resolve( this.getNewNotificationCountData( data ) );
},
get: function() {
return new $.Deferred().resolve( {
var data = this.getData();
if ( this.mode === 1 ) {
data.query.notifications.message = {
index: [ 100 ],
list: {
100: {
'*': 'Jon sent you a message on the Flow board Talk:XYZ',
read: '20140805211446',
category: 'message',
id: 100,
type: 'message'
}
}
};
}
return $.Deferred().resolve( data );
},
getNewNotificationCountData: function( data ) {
var alertCount, messageCount,
rawCount = 0,
count = 0;
messageCount = {
count: '0',
rawcount: 0
};
if ( data.list === '100' ) {
alertCount = {
count: '1',
rawcount: 1
};
count = 1;
rawCount = 1;
} else {
alertCount = {
count: '0',
rawcount: 0
};
}
data = {
query: {
echomarkread: {
alert: alertCount,
message: messageCount,
rawcount: rawCount,
count: count
}
}
};
return data;
},
getData: function() {
return {
query: {
notifications: {
index: [ 70, 71 ],
count: '1',
rawcount: 1,
list: {
70: {
'*': 'Jon mentioned you.',
agent: { id: 212, name: 'Jon' },
category: 'mention',
id: 70,
read: '20140805211446',
timestamp: {
unix: '1407273276'
message: {
index: [],
list: {}
},
alert: {
index: [ 70, 71 ],
list: {
70: {
'*': 'Jon mentioned you.',
agent: { id: 212, name: 'Jon' },
category: 'mention',
id: 70,
read: '20140805211446',
timestamp: {
unix: '1407273276'
},
title: {
full: 'Spiders'
},
type: 'mention'
},
title: {
full: 'Spiders'
},
type: 'mention'
},
71: {
'*': 'X talked to you.',
category: 'edit-user-talk',
id: 71,
type: 'edit-user-talk'
71: {
'*': 'X talked to you.',
category: 'edit-user-talk',
id: 71,
type: 'edit-user-talk'
}
}
}
}
}
} );
};
}
};
this.sandbox.stub( mw.echo.overlay, 'api', new ApiStub() );
this.ApiStub = ApiStub;
}
} );
QUnit.test( 'mw.echo.overlay.buildOverlay', 5, function( assert ) {
QUnit.test( 'mw.echo.overlay.buildOverlay', 7, function( assert ) {
var $overlay;
this.sandbox.stub( mw.echo.overlay, 'api', new this.ApiStub() );
mw.echo.overlay.buildOverlay( function( $o ) {
$overlay = $o;
} );
assert.strictEqual( $overlay.find( 'ul' ).length, 1, 'Overlay contains a list of notifications.' );
assert.strictEqual( $overlay.find( 'li' ).length, 2, 'There are two notifications.' );
assert.strictEqual( $overlay.find( '.mw-echo-overlay-title ul li' ).length, 1, 'Only one tab in header' );
assert.strictEqual( $overlay.find( 'ul.mw-echo-notifications' ).length, 1, 'Overlay contains a list of notifications.' );
assert.strictEqual( $overlay.find( 'ul.mw-echo-notifications li' ).length, 2, 'There are two notifications.' );
assert.strictEqual( $overlay.find( '.mw-echo-unread' ).length, 1, 'There is one unread notification.' );
assert.strictEqual( $overlay.find( '#mw-echo-overlay-footer a' ).length, 2,
'There is a footer with 2 links to preferences and all notifications.' );
assert.strictEqual( $overlay.find( 'a#mw-echo-overlay-moreinfo-link' ).length, 1,
'There is a help link.' );
assert.strictEqual( this.$badge.text(),
'0', 'The alerts are marked as read once opened.' );
assert.strictEqual( this.$badge.hasClass( 'mw-echo-unread-notifications' ),
false, 'The badge no longer indicates new messages.' );
} );
QUnit.test( 'mw.echo.overlay.buildOverlay with messages', 5, function( assert ) {
var $overlay;
this.sandbox.stub( mw.echo.overlay, 'api', new this.ApiStub( 1 ) );
mw.echo.overlay.buildOverlay( function( $o ) {
$overlay = $o;
} );
assert.strictEqual( $overlay.find( '.mw-echo-overlay-title ul li' ).length, 2, 'There are two tabs in header' );
assert.strictEqual( $overlay.find( '.mw-echo-notifications' ).length, 2, 'Overlay contains 2 lists of notifications.' );
assert.strictEqual( $overlay.find( '.mw-echo-overlay-title li' ).eq( 0 ).hasClass( 'mw-echo-section-current' ),
true, 'First tab is the selected tab upon opening.' );
assert.strictEqual( this.$badge.text(),
'1', 'The label stays as 1 until you switch tabs.' );
assert.strictEqual( this.$badge.hasClass( 'mw-echo-unread-notifications' ),
true, 'The notification button class is not updated until you switch to alert tab.' );
} );
QUnit.test( 'Switch tabs on overlay. No unread messages, 1 unread alert.', 4, function( assert ) {
var $overlay, $tabs;
this.sandbox.stub( mw.echo.overlay, 'api', new this.ApiStub( 1 ) );
mw.echo.overlay.buildOverlay( function( $o ) {
$overlay = $o;
$tabs = $overlay.find( '.mw-echo-overlay-title li' );
} );
// switch to 2nd tab
$overlay.find( '.mw-echo-overlay-title li' ).eq( 1 ).trigger( 'click' );
assert.strictEqual( $tabs.eq( 0 ).hasClass( 'mw-echo-section-current' ),
false, 'First tab is now the selected tab.' );
assert.strictEqual( $tabs.eq( 1 ).hasClass( 'mw-echo-section-current' ),
true, 'Second tab is now the selected tab.' );
assert.strictEqual( this.$badge.text(),
'0', 'The label is now set to 0.' );
assert.strictEqual( this.$badge.hasClass( 'mw-echo-unread-notifications' ),
false, 'There are now zero unread notifications.' );
} );
}( jQuery, mediaWiki ) );