diff options
Diffstat (limited to 'Echo/modules')
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 Binary files differnew file mode 100644 index 00000000..74a191f5 --- /dev/null +++ b/Echo/modules/icons/CrossReferenced.png diff --git a/Echo/modules/icons/Deletion.png b/Echo/modules/icons/Deletion.png Binary files differnew file mode 100644 index 00000000..8abc2e3d --- /dev/null +++ b/Echo/modules/icons/Deletion.png diff --git a/Echo/modules/icons/Featured.png b/Echo/modules/icons/Featured.png Binary files differnew file mode 100644 index 00000000..349892a7 --- /dev/null +++ b/Echo/modules/icons/Featured.png diff --git a/Echo/modules/icons/Generic.png b/Echo/modules/icons/Generic.png Binary files differnew file mode 100644 index 00000000..36a9fb1b --- /dev/null +++ b/Echo/modules/icons/Generic.png diff --git a/Echo/modules/icons/Gratitude.png b/Echo/modules/icons/Gratitude.png Binary files differnew file mode 100644 index 00000000..d22e8b63 --- /dev/null +++ b/Echo/modules/icons/Gratitude.png diff --git a/Echo/modules/icons/NotificationsPage-ltr.png b/Echo/modules/icons/NotificationsPage-ltr.png Binary files differnew file mode 100644 index 00000000..065be02c --- /dev/null +++ b/Echo/modules/icons/NotificationsPage-ltr.png diff --git a/Echo/modules/icons/NotificationsPage-rtl.png b/Echo/modules/icons/NotificationsPage-rtl.png Binary files differnew file mode 100644 index 00000000..09370026 --- /dev/null +++ b/Echo/modules/icons/NotificationsPage-rtl.png diff --git a/Echo/modules/icons/Revert.png b/Echo/modules/icons/Revert.png Binary files differnew file mode 100644 index 00000000..426ee050 --- /dev/null +++ b/Echo/modules/icons/Revert.png diff --git a/Echo/modules/icons/Reviewed.png b/Echo/modules/icons/Reviewed.png Binary files differnew file mode 100644 index 00000000..43cdd55f --- /dev/null +++ b/Echo/modules/icons/Reviewed.png diff --git a/Echo/modules/icons/ReviewedWithTags.png b/Echo/modules/icons/ReviewedWithTags.png Binary files differnew file mode 100644 index 00000000..c18a2b8d --- /dev/null +++ b/Echo/modules/icons/ReviewedWithTags.png diff --git a/Echo/modules/icons/Settings.png b/Echo/modules/icons/Settings.png Binary files differnew file mode 100644 index 00000000..f6cfc7a1 --- /dev/null +++ b/Echo/modules/icons/Settings.png diff --git a/Echo/modules/icons/Talk.png b/Echo/modules/icons/Talk.png Binary files differnew file mode 100644 index 00000000..41387cc0 --- /dev/null +++ b/Echo/modules/icons/Talk.png 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 Binary files differnew file mode 100644 index 00000000..5a7133d2 --- /dev/null +++ b/Echo/modules/overlay/Help.png diff --git a/Echo/modules/overlay/PokeyNorth.png b/Echo/modules/overlay/PokeyNorth.png Binary files differnew file mode 100644 index 00000000..af3f5ab6 --- /dev/null +++ b/Echo/modules/overlay/PokeyNorth.png 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">×</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 Binary files differnew file mode 100644 index 00000000..a911d99d --- /dev/null +++ b/Echo/modules/special/Feedback.png diff --git a/Echo/modules/special/FeedbackHover.png b/Echo/modules/special/FeedbackHover.png Binary files differnew file mode 100644 index 00000000..c46f04a0 --- /dev/null +++ b/Echo/modules/special/FeedbackHover.png diff --git a/Echo/modules/special/Help.png b/Echo/modules/special/Help.png Binary files differnew file mode 100644 index 00000000..e3de9a51 --- /dev/null +++ b/Echo/modules/special/Help.png diff --git a/Echo/modules/special/MoreInfo.png b/Echo/modules/special/MoreInfo.png Binary files differnew file mode 100644 index 00000000..6efb4473 --- /dev/null +++ b/Echo/modules/special/MoreInfo.png diff --git a/Echo/modules/special/MoreInfoHover.png b/Echo/modules/special/MoreInfoHover.png Binary files differnew file mode 100644 index 00000000..7607a7f0 --- /dev/null +++ b/Echo/modules/special/MoreInfoHover.png diff --git a/Echo/modules/special/Preferences.png b/Echo/modules/special/Preferences.png Binary files differnew file mode 100644 index 00000000..ff892422 --- /dev/null +++ b/Echo/modules/special/Preferences.png 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; + } + } + } +} |