mediawiki-extensions-Echo/modules/ui/mw.echo.ui.NotificationBadgeWidget.js
Moriel Schottlender e9264022a7 (re)Add JavaScript hooks to Notifications
Added the following Javascript hooks:
* ext.echo.notifications.beforeRender: Firing before a group of
  notification widgets are rendered, whether in the popup, in
  the special page, or in a cross-wiki bundle (which requires
  async loading)
* ext.echo.badge.countChange: Fired when the badge count changes
  with the notification type, count and the label count for
  display purposes.
* ext.echo.popup.onInitialize: Fired when the popup is opened and
  after notifications were fetched, with the context of the popup
  notification type.
* ext.echo.special.onInitialize: Fired when the special page is
  ready and notifications were fetched. Note that it will be fired
  whenever the special page is updated with notifications list,
  as well, like when changing filter, remote wiki or pagination.

The hooks were also documented in hooks.txt

Bug: T146296
Change-Id: Ie3dc97f97e8d1f90b67f62fcdc65dd29cb379aad
2016-10-14 17:25:25 -05:00

366 lines
12 KiB
JavaScript

( function ( mw, $ ) {
/**
* Notification badge button widget for echo popup.
*
* @class
* @extends OO.ui.ButtonWidget
*
* @constructor
* @param {mw.echo.Controller} controller Echo notifications controller
* @param {mw.echo.dm.ModelManager} manager Model manager
* @param {Object} [config] Configuration object
* @cfg {string|string[]} [type='message'] The type or array of types of
* notifications that are in this model. They can be 'alert', 'message' or
* an array of both. Defaults to 'message'
* @cfg {number} [numItems=0] The number of items that are in the button display
* @cfg {string} [badgeLabel=0] The initial label for the badge. This is the
* formatted version of the number of items in the badge.
* @cfg {boolean} [hasUnseen=false] Whether there are unseen items
* @cfg {number} [popupWidth=450] The width of the popup
* @cfg {string} [badgeIcon] Icon to use for the popup header
* @cfg {string} [href] URL the badge links to
* @cfg {jQuery} [$overlay] A jQuery element functioning as an overlay
* for popups.
*/
mw.echo.ui.NotificationBadgeWidget = function MwEchoUiNotificationBadgeButtonPopupWidget( controller, manager, config ) {
var buttonFlags, allNotificationsButton, preferencesButton, footerButtonGroupWidget, $footer,
notice, adjustedTypeString;
config = config || {};
config.links = config.links || {};
// Parent constructor
mw.echo.ui.NotificationBadgeWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.PendingElement.call( this, config );
this.$overlay = config.$overlay || this.$element;
// Create a menu overlay
this.$menuOverlay = $( '<div>' )
.addClass( 'mw-echo-ui-NotificationBadgeWidget-overlay-menu' );
this.$overlay.append( this.$menuOverlay );
// Controller
this.controller = controller;
this.manager = manager;
adjustedTypeString = this.controller.getTypeString() === 'message' ? 'notice' : this.controller.getTypeString();
// Properties
this.types = this.manager.getTypes();
this.numItems = config.numItems || 0;
this.badgeLabel = config.badgeLabel || this.numItems;
this.hasRunFirstTime = false;
buttonFlags = [];
if ( !!config.hasUnseen ) {
buttonFlags.push( 'unseen' );
}
this.badgeButton = new mw.echo.ui.BadgeLinkWidget( {
label: this.badgeLabel,
type: this.manager.getTypeString(),
numItems: this.numItems,
flags: buttonFlags,
// The following messages can be used here:
// tooltip-pt-notifications-alert
// tooltip-pt-notifications-notice
title: mw.msg( 'tooltip-pt-notifications-' + adjustedTypeString ),
href: config.href
} );
// Notifications list widget
this.notificationsWidget = new mw.echo.ui.NotificationsListWidget(
this.controller,
this.manager,
{
type: this.types,
$overlay: this.$menuOverlay,
animated: true
}
);
// Footer
allNotificationsButton = new OO.ui.ButtonWidget( {
icon: 'next',
label: mw.msg( 'echo-overlay-link' ),
href: config.links.notifications,
classes: [ 'mw-echo-ui-notificationBadgeButtonPopupWidget-footer-allnotifs' ]
} );
preferencesButton = new OO.ui.ButtonWidget( {
icon: 'advanced',
label: mw.msg( 'mypreferences' ),
href: config.links.preferences,
classes: [ 'mw-echo-ui-notificationBadgeButtonPopupWidget-footer-preferences' ]
} );
footerButtonGroupWidget = new OO.ui.ButtonGroupWidget( {
items: [ allNotificationsButton, preferencesButton ],
classes: [ 'mw-echo-ui-notificationBadgeButtonPopupWidget-footer-buttons' ]
} );
$footer = $( '<div>' )
.addClass( 'mw-echo-ui-notificationBadgeButtonPopupWidget-footer' )
.append( footerButtonGroupWidget.$element );
// Footer notice
if (
mw.config.get( 'wgEchoShowSpecialPageInvitation' ) &&
!mw.user.options.get( 'echo-dismiss-special-page-invitation' )
) {
notice = new mw.echo.ui.FooterNoticeWidget( {
// This is probably not the right way of doing this
iconUrl: mw.config.get( 'wgExtensionAssetsPath' ) + '/Echo/modules/icons/feedback.svg',
message: mw.message(
'echo-popup-footer-special-page-invitation',
// Text
mw.msg( 'echo-popup-footer-special-page-invitation-link' ),
// Link
mw.util.getUrl( 'Special:Notifications' )
).parse()
} );
// Event
notice.connect( this, { dismiss: 'onFooterNoticeDismiss' } );
// Prepend to the footer
$footer.prepend( notice.$element );
}
this.popup = new OO.ui.PopupWidget( {
$content: this.notificationsWidget.$element,
$footer: $footer,
width: config.popupWidth || 500,
autoClose: true,
containerPadding: 20,
// Also ignore clicks from the nested action menu items, that
// actually exist in the overlay
$autoCloseIgnore: this.$element.add( this.$menuOverlay ),
head: true,
// The following messages can be used here:
// echo-notification-alert-text-only
// echo-notification-notice-text-only
label: mw.msg(
'echo-notification-' + adjustedTypeString +
'-text-only'
),
classes: [ 'mw-echo-ui-notificationBadgeButtonPopupWidget-popup' ]
} );
// HACK: Add an icon to the popup head label
this.popupHeadIcon = new OO.ui.IconWidget( { icon: config.badgeIcon } );
this.popup.$head.prepend( this.popupHeadIcon.$element );
this.setPendingElement( this.popup.$head );
// Mark all as read button
this.markAllReadButton = new OO.ui.ButtonWidget( {
framed: false,
label: mw.msg( 'echo-mark-all-as-read' ),
classes: [ 'mw-echo-ui-notificationsWidget-markAllReadButton' ]
} );
// Hide the close button
this.popup.closeButton.toggle( false );
// Add the 'mark all as read' button to the header
this.popup.$head.append( this.markAllReadButton.$element );
this.markAllReadButton.toggle( false );
// Events
this.markAllReadButton.connect( this, { click: 'onMarkAllReadButtonClick' } );
this.manager.connect( this, {
update: 'updateBadge'
} );
this.manager.getSeenTimeModel().connect( this, { update: 'onSeenTimeModelUpdate' } );
this.manager.getUnreadCounter().connect( this, { countChange: 'updateBadge' } );
this.popup.connect( this, { toggle: 'onPopupToggle' } );
this.badgeButton.connect( this, {
click: 'onBadgeButtonClick'
} );
this.$element
.prop( 'id', 'pt-notifications-' + adjustedTypeString )
// The following classes can be used here:
// mw-echo-ui-notificationBadgeButtonPopupWidget-alert
// mw-echo-ui-notificationBadgeButtonPopupWidget-message
.addClass(
'mw-echo-ui-notificationBadgeButtonPopupWidget ' +
'mw-echo-ui-notificationBadgeButtonPopupWidget-' + adjustedTypeString
)
.append(
this.badgeButton.$element,
this.popup.$element
);
};
/* Initialization */
OO.inheritClass( mw.echo.ui.NotificationBadgeWidget, OO.ui.Widget );
OO.mixinClass( mw.echo.ui.NotificationBadgeWidget, OO.ui.mixin.PendingElement );
/* Static properties */
mw.echo.ui.NotificationBadgeWidget.static.tagName = 'li';
/* Events */
/**
* @event allRead
* All notifications were marked as read
*/
/**
* @event finishLoading
* Notifications have successfully finished being processed and are fully loaded
*/
/* Methods */
mw.echo.ui.NotificationBadgeWidget.prototype.onFooterNoticeDismiss = function () {
// Clip again to recalculate height
this.popup.clip();
// Save the preference in general
new mw.Api().saveOption( 'echo-dismiss-special-page-invitation', 1 );
// Save the preference for this session
mw.user.options.set( 'echo-dismiss-special-page-invitation', 1 );
};
/**
* Respond to badge button click
*/
mw.echo.ui.NotificationBadgeWidget.prototype.onBadgeButtonClick = function () {
this.popup.toggle();
};
/**
* Respond to SeenTime model update event
*/
mw.echo.ui.NotificationBadgeWidget.prototype.onSeenTimeModelUpdate = function () {
this.updateBadgeSeenState( false );
};
/**
* Update the badge style to match whether it contains unseen notifications.
*
* @param {boolean} [hasUnseen=false] There are unseen notifications
*/
mw.echo.ui.NotificationBadgeWidget.prototype.updateBadgeSeenState = function ( hasUnseen ) {
hasUnseen = hasUnseen === undefined ? false : !!hasUnseen;
this.badgeButton.setFlags( { unseen: !!hasUnseen } );
};
/**
* Update the badge state and label based on changes to the model
*/
mw.echo.ui.NotificationBadgeWidget.prototype.updateBadge = function () {
var unreadCount, cappedUnreadCount, badgeLabel;
unreadCount = this.manager.getUnreadCounter().getCount();
cappedUnreadCount = this.manager.getUnreadCounter().getCappedNotificationCount( unreadCount );
cappedUnreadCount = mw.language.convertNumber( cappedUnreadCount );
badgeLabel = mw.message( 'echo-badge-count', mw.language.convertNumber( cappedUnreadCount ) ).text();
this.badgeButton.setLabel( badgeLabel );
this.badgeButton.setCount( unreadCount, badgeLabel );
// Update seen state only if the counter is 0
// so we don't run into inconsistencies and have an unseen state
// for the badge with 0 unread notifications
if ( unreadCount === 0 ) {
this.updateBadgeSeenState( false );
}
// Check if we need to display the 'mark all unread' button
this.markAllReadButton.toggle( this.manager.hasLocalUnread() );
};
/**
* Respond to 'mark all as read' button click
*/
mw.echo.ui.NotificationBadgeWidget.prototype.onMarkAllReadButtonClick = function () {
// Log the click action
mw.echo.logger.logInteraction(
mw.echo.Logger.static.actions.markAllReadClick,
mw.echo.Logger.static.context.popup,
null, // Event id isn't relevant
this.manager.getTypeString() // The type of the list
);
this.controller.markLocalNotificationsRead();
};
/**
* Extend the response to button click so we can also update the notification list.
*
* @fires finishLoading
*/
mw.echo.ui.NotificationBadgeWidget.prototype.onPopupToggle = function ( isVisible ) {
var widget = this;
if ( this.promiseRunning ) {
return;
}
if ( !isVisible ) {
widget.notificationsWidget.resetInitiallyUnseenItems();
return;
}
// Log the click event
mw.echo.logger.logInteraction(
'ui-badge-link-click',
mw.echo.Logger.static.context.badge,
null,
this.controller.getTypeString()
);
if ( this.hasRunFirstTime ) {
// HACK: Clippable doesn't resize the clippable area when
// it calculates the new size. Since the popup contents changed
// and the popup is "empty" now, we need to manually set its
// size to 1px so the clip calculations will resize it properly.
// See bug report: https://phabricator.wikimedia.org/T110759
this.popup.$clippable.css( 'height', '1px' );
this.popup.clip();
}
this.pushPending();
this.markAllReadButton.toggle( false );
this.promiseRunning = true;
// Always populate on popup open. The model and widget should handle
// the case where the promise is already underway.
this.controller.fetchLocalNotifications( this.hasRunFirstTime )
.then(
// Success
function () {
if ( widget.popup.isVisible() ) {
// Fire initialization hook
mw.hook( 'ext.echo.popup.onInitialize' ).fire( widget.manager.getTypeString(), widget.controller );
widget.popup.clip();
// Update seen time
return widget.controller.updateSeenTime();
}
},
// Failure
function ( errorObj ) {
if ( errorObj.errCode === 'notlogin-required' ) {
// Login required message
widget.notificationsWidget.resetLoadingOption( mw.msg( 'echo-notification-loginrequired' ) );
} else {
// Generic API failure message
widget.notificationsWidget.resetLoadingOption( mw.msg( 'echo-api-failure' ) );
}
}
)
.then( this.emit.bind( this, 'finishLoading' ) )
.always( function () {
// Pop pending
widget.popPending();
widget.promiseRunning = false;
} );
this.hasRunFirstTime = true;
};
} )( mediaWiki, jQuery );