mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Echo
synced 2024-12-02 19:36:48 +00:00
e9264022a7
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
366 lines
12 KiB
JavaScript
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 );
|