summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Echo/modules')
-rw-r--r--Echo/modules/alert/ext.echo.alert.less12
-rw-r--r--Echo/modules/alert/ext.echo.alert.modern.css4
-rw-r--r--Echo/modules/alert/ext.echo.alert.monobook.css4
-rw-r--r--Echo/modules/badge/ext.echo.badge.less30
-rw-r--r--Echo/modules/badge/ext.echo.badge.modern.css4
-rw-r--r--Echo/modules/badge/ext.echo.badge.monobook.css3
-rw-r--r--Echo/modules/base/ext.echo.base.js84
-rw-r--r--Echo/modules/base/ext.echo.base.less53
-rw-r--r--Echo/modules/hooks.txt7
-rw-r--r--Echo/modules/icons/CrossReferenced.pngbin0 -> 322 bytes
-rw-r--r--Echo/modules/icons/Deletion.pngbin0 -> 291 bytes
-rw-r--r--Echo/modules/icons/Featured.pngbin0 -> 337 bytes
-rw-r--r--Echo/modules/icons/Generic.pngbin0 -> 536 bytes
-rw-r--r--Echo/modules/icons/Gratitude.pngbin0 -> 3231 bytes
-rw-r--r--Echo/modules/icons/NotificationsPage-ltr.pngbin0 -> 235 bytes
-rw-r--r--Echo/modules/icons/NotificationsPage-rtl.pngbin0 -> 305 bytes
-rw-r--r--Echo/modules/icons/Revert.pngbin0 -> 324 bytes
-rw-r--r--Echo/modules/icons/Reviewed.pngbin0 -> 309 bytes
-rw-r--r--Echo/modules/icons/ReviewedWithTags.pngbin0 -> 257 bytes
-rw-r--r--Echo/modules/icons/Settings.pngbin0 -> 349 bytes
-rw-r--r--Echo/modules/icons/Talk.pngbin0 -> 274 bytes
-rw-r--r--Echo/modules/mixins.less20
-rw-r--r--Echo/modules/overlay/Help.pngbin0 -> 3237 bytes
-rw-r--r--Echo/modules/overlay/PokeyNorth.pngbin0 -> 207 bytes
-rw-r--r--Echo/modules/overlay/ext.echo.overlay.init.js74
-rw-r--r--Echo/modules/overlay/ext.echo.overlay.js429
-rw-r--r--Echo/modules/overlay/ext.echo.overlay.less208
-rw-r--r--Echo/modules/overlay/ext.echo.overlay.modern.css37
-rw-r--r--Echo/modules/overlay/ext.echo.overlay.monobook.css18
-rw-r--r--Echo/modules/special/Feedback.pngbin0 -> 435 bytes
-rw-r--r--Echo/modules/special/FeedbackHover.pngbin0 -> 424 bytes
-rw-r--r--Echo/modules/special/Help.pngbin0 -> 457 bytes
-rw-r--r--Echo/modules/special/MoreInfo.pngbin0 -> 303 bytes
-rw-r--r--Echo/modules/special/MoreInfoHover.pngbin0 -> 313 bytes
-rw-r--r--Echo/modules/special/Preferences.pngbin0 -> 3128 bytes
-rw-r--r--Echo/modules/special/ext.echo.special.js173
-rw-r--r--Echo/modules/special/ext.echo.special.less94
37 files changed, 1254 insertions, 0 deletions
diff --git a/Echo/modules/alert/ext.echo.alert.less b/Echo/modules/alert/ext.echo.alert.less
new file mode 100644
index 00000000..510fdf7d
--- /dev/null
+++ b/Echo/modules/alert/ext.echo.alert.less
@@ -0,0 +1,12 @@
+#pt-mytalk {
+ white-space: nowrap;
+
+ /* High-specificity rule to override core styles for #p-personal li a */
+ a.mw-echo-alert {
+ border-radius: 2px;
+ background-color: #F9C557;
+ padding: 0.25em 0.8em 0.2em 0.8em;
+ color: #555555;
+ font-weight: normal;
+ }
+}
diff --git a/Echo/modules/alert/ext.echo.alert.modern.css b/Echo/modules/alert/ext.echo.alert.modern.css
new file mode 100644
index 00000000..66e440bb
--- /dev/null
+++ b/Echo/modules/alert/ext.echo.alert.modern.css
@@ -0,0 +1,4 @@
+/* No rounded corners for Modern skin */
+#pt-mytalk a.mw-echo-alert {
+ border-radius: 0;
+}
diff --git a/Echo/modules/alert/ext.echo.alert.monobook.css b/Echo/modules/alert/ext.echo.alert.monobook.css
new file mode 100644
index 00000000..97ee676c
--- /dev/null
+++ b/Echo/modules/alert/ext.echo.alert.monobook.css
@@ -0,0 +1,4 @@
+/* Different background color on hover for consistency with Monobook skin */
+#pt-mytalk a.mw-echo-alert:hover {
+ background-color: #FAB951;
+}
diff --git a/Echo/modules/badge/ext.echo.badge.less b/Echo/modules/badge/ext.echo.badge.less
new file mode 100644
index 00000000..d75a67f0
--- /dev/null
+++ b/Echo/modules/badge/ext.echo.badge.less
@@ -0,0 +1,30 @@
+/* We have to include the #pt-notifications selector due to monobook */
+#pt-notifications .mw-echo-notifications-badge {
+ min-width: 7px;
+ border-radius: 2px;
+ padding: 0.25em 0.45em 0.2em 0.45em;
+ margin-left: -4px;
+ text-align: center;
+ background-color: #d2d2d2;
+ font-weight: bold;
+ color: white;
+ cursor: pointer;
+ text-decoration: none;
+
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: #c2c2c2;
+ outline: none;
+ -moz-outline-style: none;
+ }
+
+ &.mw-echo-unread-notifications {
+ background-color: #cc0000;
+ }
+
+ &.mw-echo-unread-notifications:hover {
+ background-color: #bf0000;
+ }
+}
+
diff --git a/Echo/modules/badge/ext.echo.badge.modern.css b/Echo/modules/badge/ext.echo.badge.modern.css
new file mode 100644
index 00000000..6bf18c9e
--- /dev/null
+++ b/Echo/modules/badge/ext.echo.badge.modern.css
@@ -0,0 +1,4 @@
+/* No rounded corners for modern skin */
+#pt-notifications .mw-echo-notifications-badge {
+ border-radius: 0;
+}
diff --git a/Echo/modules/badge/ext.echo.badge.monobook.css b/Echo/modules/badge/ext.echo.badge.monobook.css
new file mode 100644
index 00000000..900f55b1
--- /dev/null
+++ b/Echo/modules/badge/ext.echo.badge.monobook.css
@@ -0,0 +1,3 @@
+#p-personal #pt-notifications a.mw-echo-notifications-badge:hover {
+ background-color: #c2c2c2;
+}
diff --git a/Echo/modules/base/ext.echo.base.js b/Echo/modules/base/ext.echo.base.js
new file mode 100644
index 00000000..da3a90fb
--- /dev/null
+++ b/Echo/modules/base/ext.echo.base.js
@@ -0,0 +1,84 @@
+( function ( mw, $ ) {
+ 'use strict';
+
+ mw.echo = {
+
+ clickThroughEnabled: mw.config.get( 'wgEchoConfig' ).eventlogging.EchoInteraction.enabled,
+
+ /**
+ * Set up event logging for individual notification
+ * @param {jQuery} notification JQuery representing a single notification
+ * @param {string} context 'flyout'/'archive'
+ * @param {boolean} [mobile] True if interaction was on a mobile device
+ */
+ setupNotificationLogging: function ( notification, context, mobile ) {
+ var eventId = +notification.attr( 'data-notification-event' ),
+ eventType = notification.attr( 'data-notification-type' );
+
+ // Check if Schema:EchoInteraction is enabled
+ if ( !mw.echo.clickThroughEnabled ) {
+ return;
+ }
+ // Log the impression
+ mw.echo.logInteraction( 'notification-impression', context, eventId, eventType, mobile );
+ // Set up logging for clickthrough
+ notification.find( 'a' ).click( function () {
+ mw.echo.logInteraction( 'notification-link-click', context, eventId, eventType, mobile );
+ } );
+ },
+
+ /**
+ * Log all Echo interaction related events
+ * @param {string} action The interaction
+ * @param {string} [context] 'flyout'/'archive' or undefined for the badge
+ * @param {int} [eventId] Notification event id
+ * @param {string} [eventType] notification type
+ * @param {boolean} [mobile] True if interaction was on a mobile device
+ */
+ logInteraction: function ( action, context, eventId, eventType, mobile ) {
+ // Check if Schema:EchoInteraction is enabled
+ if ( !mw.echo.clickThroughEnabled ) {
+ return;
+ }
+
+ var myEvt = {
+ action: action
+ };
+
+ // All the fields below are optional
+ if ( context ) {
+ myEvt.context = context;
+ }
+ if ( eventId ) {
+ myEvt.eventId = eventId;
+ }
+ if ( eventType ) {
+ myEvt.notificationType = eventType;
+ }
+ if ( mobile ) {
+ myEvt.mobile = mobile;
+ }
+ 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' );
+ }
+
+ };
+
+ if ( mw.echo.clickThroughEnabled ) {
+ mw.loader.using( 'ext.eventLogging', function() {
+ mw.eventLog.setDefaults( 'EchoInteraction', {
+ version: mw.config.get( 'wgEchoConfig' ).version,
+ userId: +mw.config.get( 'wgUserId' ),
+ editCount: +mw.config.get( 'wgUserEditCount' )
+ } );
+ } );
+ }
+} )( mediaWiki, jQuery );
diff --git a/Echo/modules/base/ext.echo.base.less b/Echo/modules/base/ext.echo.base.less
new file mode 100644
index 00000000..2bcca03e
--- /dev/null
+++ b/Echo/modules/base/ext.echo.base.less
@@ -0,0 +1,53 @@
+.mw-echo-title {
+ font-size: 1em;
+ line-height: 1.4em;
+}
+
+.mw-echo-content {
+ overflow: hidden;
+}
+
+.mw-echo-payload {
+ margin-top: 0.3em;
+}
+/* Including .mw-echo-timestamp for backwards compat */
+.mw-echo-timestamp, .mw-echo-notification-footer {
+ color: #6D6D6D;
+ font-size: 11px;
+ margin-top: 0.2em;
+}
+.mw-echo-notifications {
+ background-color: #EEEEEE;
+}
+.mw-echo-notification {
+ clear: both;
+ display: block;
+ color: #6D6D6D;
+ line-height: 90%;
+ margin: 0;
+ min-height: 30px;
+ background-color: white;
+ position: relative;
+ padding-top: 15px;
+ padding-bottom: 10px;
+ /* Force container to expand to height of floated contents */
+ overflow: hidden;
+ zoom: 1;
+
+ &.mw-echo-unread {
+ color: #252525;
+ }
+
+ span.autocomment {
+ color: inherit;
+ font-style: normal;
+ }
+}
+
+.mw-echo-icon {
+ width: 30px;
+ height: 30px;
+ float: left;
+ margin-right: 10px;
+ margin-left: 10px;
+}
diff --git a/Echo/modules/hooks.txt b/Echo/modules/hooks.txt
new file mode 100644
index 00000000..bad428af
--- /dev/null
+++ b/Echo/modules/hooks.txt
@@ -0,0 +1,7 @@
+hooks.txt
+
+This documents Echo's client-side hooks:
+
+'ext.echo.overlay.beforeShowingOverlay': Before showing the Echo overlay, it is
+passed to this hook, which can modify the DOM or take other actions.
+$overlay: the jQuery-wrapped element for the overlay
diff --git a/Echo/modules/icons/CrossReferenced.png b/Echo/modules/icons/CrossReferenced.png
new file mode 100644
index 00000000..74a191f5
--- /dev/null
+++ b/Echo/modules/icons/CrossReferenced.png
Binary files differ
diff --git a/Echo/modules/icons/Deletion.png b/Echo/modules/icons/Deletion.png
new file mode 100644
index 00000000..8abc2e3d
--- /dev/null
+++ b/Echo/modules/icons/Deletion.png
Binary files differ
diff --git a/Echo/modules/icons/Featured.png b/Echo/modules/icons/Featured.png
new file mode 100644
index 00000000..349892a7
--- /dev/null
+++ b/Echo/modules/icons/Featured.png
Binary files differ
diff --git a/Echo/modules/icons/Generic.png b/Echo/modules/icons/Generic.png
new file mode 100644
index 00000000..36a9fb1b
--- /dev/null
+++ b/Echo/modules/icons/Generic.png
Binary files differ
diff --git a/Echo/modules/icons/Gratitude.png b/Echo/modules/icons/Gratitude.png
new file mode 100644
index 00000000..d22e8b63
--- /dev/null
+++ b/Echo/modules/icons/Gratitude.png
Binary files differ
diff --git a/Echo/modules/icons/NotificationsPage-ltr.png b/Echo/modules/icons/NotificationsPage-ltr.png
new file mode 100644
index 00000000..065be02c
--- /dev/null
+++ b/Echo/modules/icons/NotificationsPage-ltr.png
Binary files differ
diff --git a/Echo/modules/icons/NotificationsPage-rtl.png b/Echo/modules/icons/NotificationsPage-rtl.png
new file mode 100644
index 00000000..09370026
--- /dev/null
+++ b/Echo/modules/icons/NotificationsPage-rtl.png
Binary files differ
diff --git a/Echo/modules/icons/Revert.png b/Echo/modules/icons/Revert.png
new file mode 100644
index 00000000..426ee050
--- /dev/null
+++ b/Echo/modules/icons/Revert.png
Binary files differ
diff --git a/Echo/modules/icons/Reviewed.png b/Echo/modules/icons/Reviewed.png
new file mode 100644
index 00000000..43cdd55f
--- /dev/null
+++ b/Echo/modules/icons/Reviewed.png
Binary files differ
diff --git a/Echo/modules/icons/ReviewedWithTags.png b/Echo/modules/icons/ReviewedWithTags.png
new file mode 100644
index 00000000..c18a2b8d
--- /dev/null
+++ b/Echo/modules/icons/ReviewedWithTags.png
Binary files differ
diff --git a/Echo/modules/icons/Settings.png b/Echo/modules/icons/Settings.png
new file mode 100644
index 00000000..f6cfc7a1
--- /dev/null
+++ b/Echo/modules/icons/Settings.png
Binary files differ
diff --git a/Echo/modules/icons/Talk.png b/Echo/modules/icons/Talk.png
new file mode 100644
index 00000000..41387cc0
--- /dev/null
+++ b/Echo/modules/icons/Talk.png
Binary files differ
diff --git a/Echo/modules/mixins.less b/Echo/modules/mixins.less
new file mode 100644
index 00000000..a993b320
--- /dev/null
+++ b/Echo/modules/mixins.less
@@ -0,0 +1,20 @@
+// Begin Mixins
+
+// FIXME: Use a core mixin.
+// truncated-text
+//
+// Add the truncated-text mixin to any element where long text is
+// expected, and truncating improves the UX.
+// Can be used with .truncated-text(true) to undo text truncation.
+//
+// Use in Flow, Echo and MobileFrontend extensions.
+.truncated-text(@undo: false) when not (@undo) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.truncated-text(@undo: false) when (@undo) {
+ white-space: inherit;
+ overflow: inherit;
+ text-overflow: inherit;
+}
diff --git a/Echo/modules/overlay/Help.png b/Echo/modules/overlay/Help.png
new file mode 100644
index 00000000..5a7133d2
--- /dev/null
+++ b/Echo/modules/overlay/Help.png
Binary files differ
diff --git a/Echo/modules/overlay/PokeyNorth.png b/Echo/modules/overlay/PokeyNorth.png
new file mode 100644
index 00000000..af3f5ab6
--- /dev/null
+++ b/Echo/modules/overlay/PokeyNorth.png
Binary files differ
diff --git a/Echo/modules/overlay/ext.echo.overlay.init.js b/Echo/modules/overlay/ext.echo.overlay.init.js
new file mode 100644
index 00000000..534bebf5
--- /dev/null
+++ b/Echo/modules/overlay/ext.echo.overlay.init.js
@@ -0,0 +1,74 @@
+( function ( $, mw ) {
+ $( function () {
+ var $link = $( '#pt-notifications a' );
+ if ( ! $link.length ) {
+ return;
+ }
+
+ $link.click( function ( e ) {
+ var $target;
+
+ // log the badge click
+ mw.echo.logInteraction( 'ui-badge-link-click' );
+
+ e.preventDefault();
+
+ $target = $( e.target );
+ // If the user clicked on the overlay or any child, ignore the click
+ if ( $target.hasClass( 'mw-echo-overlay' ) || $target.is( '.mw-echo-overlay *' ) ) {
+ return;
+ }
+
+ if ( $( '.mw-echo-overlay' ).length ) {
+ mw.echo.overlay.removeOverlay();
+ return;
+ }
+
+ mw.echo.overlay.buildOverlay(
+ function ( $overlay ) {
+ $overlay
+ .hide()
+ .appendTo( document.body );
+
+ function positionOverlay() {
+ var offset = $( '#pt-notifications' ).offset();
+ $overlay.css( { left: offset.left - 190, top: offset.top + 50 } );
+ }
+ positionOverlay();
+ $( window ).on( 'resize', positionOverlay );
+ mw.hook( 'ext.echo.overlay.beforeShowingOverlay' ).fire( $overlay );
+
+ // Show the notifications overlay
+ $overlay.show();
+
+ // Make sure the overlay is visible, even if the badge is near the edge of browser window.
+ // 10 is an arbitrarily chosen "close enough" number.
+ // We are careful not to slide out from below the pokey (which is 21px wide) (200-21/2+1 == 189)
+ var
+ offset = $overlay.offset(),
+ width = $overlay.width(),
+ windowWidth = $( window ).width();
+ if ( offset.left < 10 ) {
+ $overlay.css( 'left', '+=' + Math.min( 189, 10 - offset.left ) );
+ } else if ( offset.left + width > windowWidth - 10 ) {
+ $overlay.css( 'left', '-=' + Math.min( 189, ( offset.left + width ) - ( windowWidth - 10 ) ) );
+ }
+ }
+ );
+ } );
+
+ $( 'body' ).click( function ( e ) {
+ if ( ! $( e.target ).is( '.mw-echo-overlay, .mw-echo-overlay *, #pt-notifications a' ) ) {
+ mw.echo.overlay.removeOverlay();
+ }
+ } );
+
+ // Closes the notifications overlay when ESC key pressed
+ $( document ).on( 'keydown', function ( e ) {
+ if ( e.which === 27 ) {
+ mw.echo.overlay.removeOverlay();
+ }
+ } );
+
+ } );
+}( jQuery, mediaWiki ));
diff --git a/Echo/modules/overlay/ext.echo.overlay.js b/Echo/modules/overlay/ext.echo.overlay.js
new file mode 100644
index 00000000..7d5dfca4
--- /dev/null
+++ b/Echo/modules/overlay/ext.echo.overlay.js
@@ -0,0 +1,429 @@
+/*global window:false */
+( function ( $, mw ) {
+ 'use strict';
+
+ // backwards compatibility <= MW 1.21
+ var getUrl = mw.util.getUrl || mw.util.wikiGetlink,
+ useLang = mw.config.get( 'wgUserLanguage' );
+
+ function EchoOverlay( apiResultNotifications ) {
+ this.api = mw.echo.overlay.api;
+ // set internal properties
+ this.tabs = [];
+ this._buildOverlay( apiResultNotifications );
+ }
+
+ function EchoOverlayTab( options, notifications ) {
+ this.api = mw.echo.overlay.api;
+ this.markOnView = options.markOnView;
+ this.markAsReadCallback = options.markAsReadCallback;
+ this.name = options.name;
+ this.unread = [];
+ this._totalUnread = notifications[this.name].rawcount;
+ this._buildList( notifications[this.name] );
+ }
+
+ EchoOverlayTab.prototype = {
+ /* @var integer totalUnread the number of unread notifications in this tab.
+ including those that are not visible. */
+ /**
+ * Return a list of unread and shown ids
+ * @method
+ * @param integer id of a notification to mark as read
+ * @return jQuery.Deferred
+ */
+ getUnreadIds: function() {
+ return this.unread;
+ },
+ /**
+ * Get a count the number of all unread notifications of this type
+ * @method
+ * @param integer id of a notification to mark as read
+ * @return integer
+ */
+ getNumberUnread: function() {
+ return this._totalUnread;
+ },
+ /**
+ * Mark all existing notifications as read
+ * @method
+ * @param integer id of a notification to mark as read
+ * @return jQuery.Deferred
+ */
+ markAsRead: function( id ) {
+ var self = this, data;
+ // only need to mark as read if there is unread item
+ if ( this.unread.length ) {
+ data = {
+ action: 'echomarkread',
+ token: mw.user.tokens.get( 'editToken' ),
+ uselang: useLang
+ };
+ if ( id ) {
+ // If id is given mark that as read otherwise use all unread messages
+ data.list = id;
+ } else {
+ data.sections = this.name;
+ }
+
+ return this.api.post( data ).then( function ( result ) {
+ return result.query.echomarkread;
+ } ).done( function( result ) {
+ // reset internal state of unread messages
+ if ( id ) {
+ if ( self.unread.indexOf( id ) > -1 ) {
+ self.unread.splice( self.unread.indexOf( id ), 1 );
+ }
+ } else {
+ self.unread = [];
+ }
+ // update the count
+ self._totalUnread = result[self.name].rawcount;
+ self.markAsReadCallback( result, id );
+ } );
+ } else {
+ return new $.Deferred();
+ }
+ },
+ /**
+ * Builds an Echo notifications list
+ * @method
+ * @param string tabName the tab
+ * @param object notifications as returned by the api of notification items
+ * @return jQuery element
+ */
+ _buildList: function( notifications ) {
+ var self = this,
+ $container = $( '<div class="mw-echo-notifications">' )
+ .data( 'tab', this )
+ .css( 'max-height', $( window ).height() - 140 ),
+ $ul = $( '<ul>' ).appendTo( $container );
+
+ $.each( notifications.index, function ( index, id ) {
+ var $wrapper,
+ data = notifications.list[id],
+ $li = $( '<li>' )
+ .data( 'details', data )
+ .data( 'id', id )
+ .attr( {
+ 'data-notification-category': data.category,
+ 'data-notification-event': data.id,
+ 'data-notification-type': data.type
+ } )
+ .addClass( 'mw-echo-notification' );
+
+ if ( !data['*'] ) {
+ return;
+ }
+
+ $li.append( data['*'] )
+ .appendTo( $ul );
+
+ if ( !data.read ) {
+ $li.addClass( 'mw-echo-unread' );
+ self.unread.push( id );
+ if ( !self.markOnView ) {
+ $( '<button class="mw-ui-button mw-ui-quiet">&times;</button>' )
+ .on( 'click', function( ev ) {
+ ev.preventDefault();
+ self.markAsRead( $( this ).closest( 'li' ).data( 'notification-event' ) );
+ } ).appendTo( $li );
+ }
+ }
+
+ // Grey links in the notification title and footer (except on hover)
+ $li.find( '.mw-echo-title a, .mw-echo-notification-footer a' )
+ .addClass( 'mw-echo-grey-link' );
+ $li.hover(
+ function() {
+ $( this ).find( '.mw-echo-title a, .mw-echo-notification-footer a' ).removeClass( 'mw-echo-grey-link' );
+ },
+ function() {
+ $( this ).find( '.mw-echo-title a, .mw-echo-notification-footer a' ).addClass( 'mw-echo-grey-link' );
+ }
+ );
+ // If there is a primary link, make the entire notification clickable.
+ // Yes, it is possible to nest <a> tags via DOM manipulation,
+ // and it works like one would expect.
+ if ( $li.find( '.mw-echo-notification-primary-link' ).length ) {
+ $wrapper = $( '<a>' )
+ .addClass( 'mw-echo-notification-wrapper' )
+ .attr( 'href', $li.find( '.mw-echo-notification-primary-link' ).attr( 'href' ) )
+ .click( function() {
+ if ( mw.echo.clickThroughEnabled ) {
+ // Log the clickthrough
+ mw.echo.logInteraction( 'notification-link-click', 'flyout', +data.id, data.type );
+ }
+ } );
+ } else {
+ $wrapper = $('<div>').addClass( 'mw-echo-notification-wrapper' );
+ }
+
+ $li.wrapInner( $wrapper );
+
+ mw.echo.setupNotificationLogging( $li, 'flyout' );
+
+ // Set up each individual notification with a close box and dismiss
+ // interface if it is dismissable.
+ if ( $li.find( '.mw-echo-dismiss' ).length ) {
+ mw.echo.setUpDismissability( $li );
+ }
+ } );
+
+ if ( !this.markOnView && this.unread.length ) {
+ $( '<button class="mw-ui-button mw-ui-quiet">' )
+ .text( mw.msg( 'echo-mark-all-as-read' ) )
+ .on( 'click', function() {
+ var $btn = $( this );
+ self.markAsRead().done( function() {
+ self.$el.find( '.mw-echo-unread' ).removeClass( 'mw-echo-unread' );
+ $btn.remove();
+ } );
+ } )
+ .prependTo( $container );
+ }
+ this.$el = $container;
+ }
+ };
+
+ EchoOverlay.prototype = {
+ /**
+ * @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;
+ mw.hook( 'ext.echo.updateNotificationCount' ).fire( rawCount );
+ },
+
+ configuration: mw.config.get( 'wgEchoOverlayConfiguration' ),
+
+ _getFooterElement: function() {
+ var $prefLink = $( '#pt-preferences a' ),
+ links = [
+ { url: getUrl( 'Special:Notifications' ), text: mw.msg( 'echo-overlay-link' ),
+ className: 'mw-echo-icon-all' },
+ { url: $prefLink.attr( 'href' ) + '#mw-prefsection-echo', text: $prefLink.text(),
+ className: 'mw-echo-icon-cog' }
+ ],
+ $overlayFooter = $( '<div class="mw-echo-overlay-footer">' );
+
+ $.each( links, function( i, link ) {
+ $( '<a class="mw-echo-grey-link">' )
+ .attr( 'href', link.url )
+ .addClass( link.className )
+ .text( link.text )
+ .appendTo( $overlayFooter );
+ } );
+ // add link to notifications archive
+ $overlayFooter.find( 'a' ).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();
+
+ this._activeTab = tab;
+ $lists.each( function() {
+ if ( $( this ).data( 'tab' ).name === tab.name ) {
+ $( this ).show();
+ if ( tab.markOnView ) {
+ tab.markAsRead();
+ }
+ }
+ } );
+ },
+
+
+ _updateTitleElement: function() {
+ var $header;
+ $header = this.$el.find( '.mw-echo-overlay-title' );
+ this._getTitleElement().insertBefore( $header );
+ $header.remove();
+ },
+
+ _getTabsElement: function() {
+ var $li,
+ $ul = $( '<ul>' ), self = this;
+
+ $.each( this.tabs, function( i, echoTab ) {
+ var
+ tabName = self.tabs.length > 1 ? echoTab.name : ( echoTab.name + '-text-only' ),
+ // Messages that can be used here:
+ // * echo-notification-alert
+ // * echo-notification-message
+ // * echo-notification-alert-text-only
+ // * echo-notification-message-text-only
+ // @todo: Unread value is inaccurate. If a user has more than mw.echo.overlay.notificationLimit
+ // API change needed
+ label = mw.msg(
+ 'echo-notification-' + tabName,
+ mw.language.convertNumber( echoTab.getNumberUnread() )
+ );
+
+ $li = $( '<li>' )
+ .appendTo( $ul );
+
+ $( '<a class="mw-ui-anchor mw-ui-progressive">' )
+ .on( 'click', function() {
+ var $this = $( this );
+ $ul.find( 'a' ).removeClass( 'mw-ui-quiet' ).addClass( 'mw-ui-active' );
+ $this.addClass( 'mw-ui-quiet' ).removeClass( 'mw-ui-active');
+ self._showTabList( $this.data( 'tab' ) );
+ } )
+ .data( 'tab', echoTab )
+ .addClass( echoTab.name === self._activeTab.name ? 'mw-ui-quiet' : 'mw-ui-active' )
+ .text( label ).appendTo( $li );
+ } );
+ return $ul;
+ },
+
+ getUnreadCount: function() {
+ var count = 0;
+ $.each( this.tabs, function( i, tab ) {
+ count += tab.getNumberUnread();
+ } );
+ return count;
+ },
+
+ _getTitleElement: function() {
+ var $title = $( '<div>' ).addClass( 'mw-echo-overlay-title' )
+ .append( this._getTabsElement() );
+ return $title;
+ },
+
+ _buildOverlay: function ( notifications ) {
+ var tabs,
+ self = this,
+ options = {
+ markAsReadCallback: function( data, id ) {
+ self.updateBadgeCount( data.count, data.rawcount );
+ self._updateTitleElement();
+ if ( id ) {
+ self.$el.find( '[data-notification-event="' + id + '"]').removeClass( 'mw-echo-unread' )
+ .find( 'button' ).remove();
+ }
+ }
+ },
+ $overlay = $( '<div>' ).addClass( 'mw-echo-overlay' );
+
+ this.$el = $overlay;
+
+ if ( notifications.message.index.length ) {
+ tabs = [ { name: 'alert', markOnView: true }, { name: 'message' } ];
+ } else {
+ tabs = [ { name: 'alert', markOnView: true } ];
+ }
+
+ $.each( tabs, function( i, tabOptions ) {
+ var tab = new EchoOverlayTab( $.extend( tabOptions, options ), notifications );
+ self.$el.append( tab.$el );
+ self.tabs.push( tab );
+ self.notificationCount.all += notifications[tabOptions.name].index.length;
+ } );
+
+ if ( tabs.length === 1 ) {
+ // only one tab exists
+ this._activeTab = this.tabs[0];
+ } else if (
+ notifications.message.rawcount > 0 &&
+ notifications.alert.rawcount === 0
+ ) {
+ // if there are new messages and no new alerts show the messages tab
+ this._activeTab = this.tabs[1];
+ } else {
+ // otherwise show the alerts tab
+ this._activeTab = this.tabs[0];
+ }
+
+ $overlay.prepend( this._getTitleElement() );
+ $overlay.append( this._getFooterElement() );
+ // Show the active tab.
+ this._showTabList( this._activeTab );
+ }
+ };
+
+ mw.echo.overlay = {
+ /**
+ * @var integer the maximum number of notifications to show in the overlay
+ */
+ notificationLimit: 25,
+ /**
+ * @var mw.Api
+ */
+ api: new mw.Api( { ajax: { cache: false } } ),
+ /**
+ * Create an Echo overlay
+ * @return jQuery.Deferred with new EchoOverlay passed in callback
+ */
+ getNewOverlay: function() {
+ var apiData = {
+ action: 'query',
+ meta: 'notifications',
+ notsections: 'alert|message',
+ notgroupbysection: 1,
+ notmessageunreadfirst: 1,
+ notformat: 'flyout',
+ notlimit: this.notificationLimit,
+ notprop: 'index|list|count',
+ uselang: useLang
+ };
+
+ return this.api.get( apiData ).then( function ( result ) {
+ return new EchoOverlay( result.query.notifications );
+ } );
+ },
+ /**
+ * Builds an overlay element
+ * @method
+ * @param callback a callback which passes the newly created overlay as a parameter
+ */
+ buildOverlay: function( callback ) {
+ this.getNewOverlay().done( function( overlay ) {
+ callback( overlay.$el );
+ } ).fail( function () {
+ window.location.href = $( '#pt-notifications a' ).attr( 'href' );
+ } );
+ },
+ removeOverlay: function () {
+ $( '.mw-echo-overlay' ).fadeOut( 'fast',
+ function () {
+ $( this ).remove();
+ }
+ );
+ }
+ };
+} )( jQuery, mediaWiki );
diff --git a/Echo/modules/overlay/ext.echo.overlay.less b/Echo/modules/overlay/ext.echo.overlay.less
new file mode 100644
index 00000000..930fdf59
--- /dev/null
+++ b/Echo/modules/overlay/ext.echo.overlay.less
@@ -0,0 +1,208 @@
+@import '../mixins.less';
+@import "mediawiki.mixins";
+@import "mediawiki.ui/variables";
+@import "mediawiki.ui/mixins";
+
+@offset: 200px;
+@chevronHeight: 11px;
+@headerFontSize: 13px;
+
+.mw-echo-overlay {
+ position: absolute;
+ top: 30px + @chevronHeight;
+ border: 1px solid silver;
+ background-color: #fff;
+ width: 450px;
+ min-height: 2em;
+ padding: 0;
+ color: #6D6D6D;
+ z-index: 100;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.35);
+
+ // IE8
+ &:before,
+ &::before {
+ content: '';
+ background-image: url('PokeyNorth.png');
+ background-repeat: no-repeat;
+ width: 21px;
+ height: @chevronHeight;
+ position: absolute;
+ z-index: 101;
+ top: -@chevronHeight;
+
+ /* @noflip */ body.ltr & {
+ // subtract half the width from the offset and then add the left box shadow
+ /* @noflip */ left: @offset - 10px + 3px;
+ }
+
+ /* @noflip */ body.rtl & {
+ // subtract the box shadow
+ /* @noflip */ left: @offset - 3px;
+ }
+ }
+
+ button {
+ top: 15px;
+ right: 15px;
+ position: absolute;
+ padding: 0;
+ }
+
+ .mw-ui-progressive {
+ cursor: pointer;
+
+ &.mw-ui-quiet {
+ pointer-events: none;
+ color: #6D6D6D;
+ font-weight: bold;
+ }
+ }
+
+ .mw-echo-notifications,
+ ul {
+ overflow: auto;
+ padding: 0;
+ margin: 0;
+ }
+
+ .mw-echo-notifications {
+ button {
+ // Add 1px border to 15px line height so lines up with tabs
+ line-height: 16px;
+ font-size: @headerFontSize;
+ }
+ }
+
+ li.mw-echo-notification {
+ display: block;
+ padding: 0;
+
+ &:hover {
+ .mw-echo-notification-wrapper {
+ background-color: #F9F9F9;
+ }
+ }
+
+ .mw-echo-notification-wrapper {
+ display: block;
+ background-color: #F1F1F1;
+ border-bottom: 1px solid #DDDDDD;
+ padding: 15px 40px 10px 10px;
+ white-space: normal;
+ font-size: 13px;
+ line-height: 16px;
+ /* Suppress standard links styles */
+ color: inherit;
+ text-decoration: inherit;
+ }
+
+ &.mw-echo-unread {
+ .mw-echo-notification-wrapper {
+ background-color: white;
+ }
+
+ &:hover {
+ .mw-echo-notification-wrapper {
+ background-color: #F9F9F9;
+ }
+ }
+
+ button {
+ background: none;
+ border: none;
+ }
+ }
+
+ &:last-child {
+ .mw-echo-notification-wrapper {
+ border-bottom: none;
+ }
+ }
+ }
+}
+
+.mw-echo-title {
+ color: @colorTextLight;
+
+ .mw-echo-title-heading {
+ color: @colorTextLight;
+ font-size: 1.15em;
+
+ &, a {
+ font-weight: bold;
+ }
+ }
+
+ .mw-echo-title-heading,
+ .plainlinks {
+ .truncated-text();
+ max-width: 100%;
+ display: inline-block;
+ vertical-align: top;
+ }
+}
+
+.mw-echo-grey-link {
+ color: @colorTextLight;
+}
+
+.mw-echo-notification-primary-link {
+ display: none;
+}
+
+.mw-echo-overlay-title {
+ font-size: @headerFontSize;
+ line-height: 15px;
+ padding: 15px 15px 15px 28px;
+ border-bottom: 1px solid #DDDDDD;
+
+ li {
+ display: inline;
+ font-size: 1em;
+ margin-left: 0;
+
+ &::after {
+ content: " · ";
+ padding: 0 .25em;
+ }
+
+ &:last-child {
+ &::after {
+ content: '';
+ }
+ }
+ }
+}
+
+.mw-echo-overlay-footer {
+ padding: 0;
+ border-top: 1px solid #DDDDDD;
+ display: table;
+ width: 100%;
+
+ a {
+ border-left: 1px solid #DDDDDD;
+ float: none;
+ display: table-cell;
+ min-height: 14px;
+ font-size: @headerFontSize;
+ white-space: normal;
+ font-weight: bold;
+ padding: 15px 15px 15px 45px;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+}
+
+.mw-echo-icon-all {
+ /* @embed */
+ background: url(../icons/NotificationsPage-ltr.png) no-repeat 20px 15px !important;
+}
+
+.mw-echo-icon-cog {
+ /* @embed */
+ background: url(../icons/Settings.png) no-repeat 20px 15px !important;
+}
diff --git a/Echo/modules/overlay/ext.echo.overlay.modern.css b/Echo/modules/overlay/ext.echo.overlay.modern.css
new file mode 100644
index 00000000..005e6d9e
--- /dev/null
+++ b/Echo/modules/overlay/ext.echo.overlay.modern.css
@@ -0,0 +1,37 @@
+body #p-personal {
+ overflow: visible;
+}
+
+#p-personal .mw-echo-overlay {
+ text-transform: none;
+ font-variant: normal;
+ font-weight: normal;
+ left: -20px;
+ top: 31px;
+}
+#p-personal #pt-notifications ul,
+#p-personal #pt-notifications li {
+ text-transform: none;
+ font-weight: normal;
+ height: auto;
+}
+#p-personal #mw-echo-overlay-link {
+ padding: 15px 15px 15px 60px;
+}
+#p-personal #mw-echo-overlay-pref-link {
+ padding: 15px 15px 15px 35px;
+}
+
+#p-personal .mw-echo-overlay a {
+ padding: 0;
+ color: #003366;
+}
+#p-personal .mw-echo-overlay a:visited {
+ padding: 0;
+ color: #5a3696;
+}
+#p-personal .mw-echo-overlay a:hover {
+ padding: 0;
+ color: #003366;
+ text-decoration: inherit;
+}
diff --git a/Echo/modules/overlay/ext.echo.overlay.monobook.css b/Echo/modules/overlay/ext.echo.overlay.monobook.css
new file mode 100644
index 00000000..e3fd2fe6
--- /dev/null
+++ b/Echo/modules/overlay/ext.echo.overlay.monobook.css
@@ -0,0 +1,18 @@
+#p-personal .mw-echo-overlay {
+ text-align: left;
+ text-transform: none;
+ font-weight: normal;
+}
+#p-personal .mw-echo-overlay ul {
+ text-align: left;
+ text-transform: none;
+}
+#p-personal #mw-echo-overlay-link {
+ padding-bottom: 15px;
+}
+#p-personal #mw-echo-overlay-pref-link {
+ padding-bottom: 15px;
+}
+#p-personal .mw-echo-overlay li.mw-echo-notification {
+ color: #6D6D6D;
+}
diff --git a/Echo/modules/special/Feedback.png b/Echo/modules/special/Feedback.png
new file mode 100644
index 00000000..a911d99d
--- /dev/null
+++ b/Echo/modules/special/Feedback.png
Binary files differ
diff --git a/Echo/modules/special/FeedbackHover.png b/Echo/modules/special/FeedbackHover.png
new file mode 100644
index 00000000..c46f04a0
--- /dev/null
+++ b/Echo/modules/special/FeedbackHover.png
Binary files differ
diff --git a/Echo/modules/special/Help.png b/Echo/modules/special/Help.png
new file mode 100644
index 00000000..e3de9a51
--- /dev/null
+++ b/Echo/modules/special/Help.png
Binary files differ
diff --git a/Echo/modules/special/MoreInfo.png b/Echo/modules/special/MoreInfo.png
new file mode 100644
index 00000000..6efb4473
--- /dev/null
+++ b/Echo/modules/special/MoreInfo.png
Binary files differ
diff --git a/Echo/modules/special/MoreInfoHover.png b/Echo/modules/special/MoreInfoHover.png
new file mode 100644
index 00000000..7607a7f0
--- /dev/null
+++ b/Echo/modules/special/MoreInfoHover.png
Binary files differ
diff --git a/Echo/modules/special/Preferences.png b/Echo/modules/special/Preferences.png
new file mode 100644
index 00000000..ff892422
--- /dev/null
+++ b/Echo/modules/special/Preferences.png
Binary files differ
diff --git a/Echo/modules/special/ext.echo.special.js b/Echo/modules/special/ext.echo.special.js
new file mode 100644
index 00000000..5ee0b368
--- /dev/null
+++ b/Echo/modules/special/ext.echo.special.js
@@ -0,0 +1,173 @@
+( function ( $, mw ) {
+ 'use strict';
+ var useLang = mw.config.get( 'wgUserLanguage' );
+
+ mw.echo.special = {
+
+ notcontinue: null,
+ header: '',
+ processing: false,
+
+ /**
+ * Initialize the property in special notification page.
+ */
+ initialize: function () {
+ var skin = mw.config.get('skin');
+
+ // Convert more link into a button
+ $( '#mw-echo-more' )
+ .addClass( 'mw-ui-button mw-ui-primary' )
+ .css( 'margin', '0.5em 0 0 0' )
+ .click( function ( e ) {
+ e.preventDefault();
+ if ( !mw.echo.special.processing ) {
+ mw.echo.special.processing = true;
+ mw.echo.special.loadMore();
+ }
+ }
+ );
+ mw.echo.special.notcontinue = mw.config.get( 'wgEchoNextContinue' );
+ mw.echo.special.header = mw.config.get( 'wgEchoDateHeader' );
+
+ // Set up each individual notification with eventlogging, a close
+ // box and dismiss interface if it is dismissable.
+ $( '.mw-echo-notification' ).each( function () {
+ mw.echo.setupNotificationLogging( $( this ), 'archive' );
+ if ( $( this ).find( '.mw-echo-dismiss' ).length ) {
+ mw.echo.setUpDismissability( this );
+ }
+ } );
+
+ $( '#mw-echo-moreinfo-link' ).click( function () {
+ mw.echo.logInteraction( 'ui-help-click', 'archive' );
+ } );
+ $( '#mw-echo-pref-link' ).click( function () {
+ mw.echo.logInteraction( 'ui-prefs-click', 'archive' );
+ } );
+
+ // Convert subtitle links into header icons for Vector and Monobook skins
+ if ( skin === 'vector' || skin === 'monobook' ) {
+ $( '#mw-echo-moreinfo-link, #mw-echo-pref-link' )
+ .empty()
+ .appendTo( '#firstHeading' );
+ $( '#contentSub' ).empty();
+ }
+
+ },
+
+ /**
+ * Load more notification records.
+ */
+ loadMore: function () {
+ var api = new mw.Api( { ajax: { cache: false } } ),
+ notifications, data, container, $li, that = this, unread = [], apiData;
+
+ apiData = {
+ action : 'query',
+ meta : 'notifications',
+ notformat : 'html',
+ notprop : 'index|list',
+ notcontinue: this.notcontinue,
+ notlimit: mw.config.get( 'wgEchoDisplayNum' ),
+ uselang: useLang
+ };
+
+ api.get( apiData ).done( function ( result ) {
+ container = $( '#mw-echo-special-container' );
+ notifications = result.query.notifications;
+ unread = [];
+
+ $.each( notifications.index, function ( index, id ) {
+ data = notifications.list[id];
+
+ if ( that.header !== data.timestamp.date ) {
+ that.header = data.timestamp.date;
+ $( '<li></li>' ).addClass( 'mw-echo-date-section' ).append( that.header ).appendTo( container );
+ }
+
+ $li = $( '<li></li>' )
+ .data( 'details', data )
+ .data( 'id', id )
+ .addClass( 'mw-echo-notification' )
+ .attr( {
+ 'data-notification-category': data.category,
+ 'data-notification-event': data.id,
+ 'data-notification-type': data.type
+ } )
+ .append( data['*'] )
+ .appendTo( container );
+
+ if ( !data.read ) {
+ $li.addClass( 'mw-echo-unread' );
+ unread.push( id );
+ }
+
+ mw.echo.setupNotificationLogging( $li, 'archive' );
+
+ if ( $li.find( '.mw-echo-dismiss' ).length ) {
+ mw.echo.setUpDismissability( $li );
+ }
+ } );
+
+ that.notcontinue = notifications['continue'];
+ if ( unread.length > 0 ) {
+ that.markAsRead( unread );
+ } else {
+ that.onSuccess();
+ }
+ } ).fail( function () {
+ that.onError();
+ } );
+ },
+
+ /**
+ * Mark notifications as read.
+ */
+ markAsRead: function ( unread ) {
+ var newCount, rawCount, $badge,
+ api = new mw.Api(), that = this;
+
+ api.post( {
+ action : 'echomarkread',
+ list : unread.join( '|' ),
+ token: mw.user.tokens.get( 'editToken' ),
+ uselang: useLang
+ } ).done( function ( result ) {
+ // update the badge if the link is enabled
+ if ( result.query.echomarkread.count !== undefined &&
+ $( '#pt-notifications').length
+ ) {
+ newCount = result.query.echomarkread.count;
+ rawCount = result.query.echomarkread.rawcount;
+ $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' );
+ }
+ }
+ that.onSuccess();
+ } ).fail( function () {
+ that.onError();
+ } );
+ },
+
+ onSuccess: function () {
+ if ( !this.notcontinue ) {
+ $( '#mw-echo-more' ).hide();
+ }
+ this.processing = false;
+ },
+
+ onError: function () {
+ // Todo: Show detail error message based on error code
+ $( '#mw-echo-more' ).text( mw.msg( 'echo-load-more-error' ) );
+ this.processing = false;
+ }
+ };
+
+ $( document ).ready( mw.echo.special.initialize );
+
+} )( jQuery, mediaWiki );
diff --git a/Echo/modules/special/ext.echo.special.less b/Echo/modules/special/ext.echo.special.less
new file mode 100644
index 00000000..57cb9c20
--- /dev/null
+++ b/Echo/modules/special/ext.echo.special.less
@@ -0,0 +1,94 @@
+/* Echo specific CSS */
+
+#mw-echo-more {
+ display: block;
+ text-align: center;
+ font-size: 13px;
+ max-width: 600px;
+}
+
+/* Custom header styling for Vector and Monobook skins */
+.skin-vector #firstHeading,
+.skin-monobook #firstHeading {
+ max-width: 600px;
+}
+
+/* Special styles to use if we're converting subtitle links into header icons */
+#firstHeading {
+ .mw-echo-special-header-link {
+ display: block;
+ height: 19px;
+ width: 19px;
+ }
+
+ #mw-echo-pref-link {
+ float: right;
+ margin: 5px 3px;
+ /* @embed */
+ background-image: url(Preferences.png);
+ background-repeat: no-repeat;
+ background-position: 0 0;
+ filter: alpha(opacity=50);
+ opacity: 0.5;
+
+ &:hover {
+ filter: alpha(opacity=100);
+ opacity: 1.0;
+ }
+ }
+
+ #mw-echo-moreinfo-link {
+ display: inline-block;
+ margin: 0 3px;
+ /* @embed */
+ background-image: url(Help.png);
+ background-repeat: no-repeat;
+ background-position: 0 0;
+ filter: alpha(opacity=50);
+ opacity: 0.5;
+
+ &:hover {
+ filter: alpha(opacity=100);
+ opacity: 1.0;
+ }
+ }
+}
+
+.mw-echo-date-section {
+ font-weight: 800;
+ font-size: 1.1em;
+ text-transform: uppercase;
+ border-bottom: 1px solid #C9C9C9;
+ margin: 30px 0 5px 50px;
+ color: #686868;
+ max-width: 550px;
+}
+
+ul#mw-echo-special-container {
+ list-style: none none;
+ padding: 0;
+ margin: 30px 0 0 0;
+ max-width: 600px;
+}
+
+.mw-echo-notification {
+ padding: 15px 35px 10px 0;
+}
+
+#mw-echo-special-container {
+ .mw-echo-notification {
+ background-color: transparent;
+
+ &:hover {
+ /* Fallback for IE<=8 */
+ background-color: #F6F6F6;
+ background-color: rgba(0, 0, 0, 0.035);
+ }
+
+ &.mw-echo-unread {
+ .mw-echo-title {
+ font-weight: bold;
+ }
+ }
+ }
+}