mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Echo
synced 2024-11-28 09:40:41 +00:00
0834b91f56
Split and refactor Echo network handling and create a proper API layer for the UI to use consistently. Split Echo's API methods into its own module so they can be loaded along with the initialization script and manage the API requests. Change-Id: I0526a14bb8cc0d9729a303e24ab6e43259cc86bb
351 lines
11 KiB
JavaScript
351 lines
11 KiB
JavaScript
( function ( mw, $ ) {
|
|
/**
|
|
* Notification badge button widget for echo popup.
|
|
*
|
|
* @class
|
|
* @extends OO.ui.ButtonWidget
|
|
*
|
|
* @constructor
|
|
* @param {mw.echo.dm.NotificationsModel} model Notifications view model
|
|
* @param {Object} [config] Configuration object
|
|
* @cfg {number} [numItems=0] How many items are in the button display
|
|
* @cfg {boolean} [hasUnseen=false] Whether there are unseen items
|
|
* @cfg {boolean} [markReadWhenSeen=false] Mark all notifications as read on open
|
|
* @cfg {number} [popupWidth=450] The width of the popup
|
|
* @cfg {string|Object} [badgeIcon] The icons to use for this button.
|
|
* If this is a string, it will be used as the icon regardless of the state.
|
|
* If it is an object, it must include
|
|
* the properties 'unseen' and 'seen' with icons attached to both. For example:
|
|
* { badgeIcon: {
|
|
* unseen: 'bellOn',
|
|
* seen: 'bell'
|
|
* } }
|
|
* @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( model, config ) {
|
|
var buttonFlags, allNotificationsButton, preferencesButton, footerButtonGroupWidget, $footer;
|
|
|
|
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 );
|
|
|
|
// View model
|
|
this.notificationsModel = model;
|
|
this.type = this.notificationsModel.getType();
|
|
|
|
this.numItems = config.numItems || 0;
|
|
this.markReadWhenSeen = !!config.markReadWhenSeen;
|
|
this.badgeIcon = config.badgeIcon || {};
|
|
this.hasRunFirstTime = false;
|
|
this.currentUnreadCountInBadge = 0;
|
|
|
|
buttonFlags = [ 'primary' ];
|
|
if ( !!config.hasUnseen ) {
|
|
buttonFlags.push( 'unseen' );
|
|
}
|
|
|
|
this.badgeButton = new mw.echo.ui.BadgeLinkWidget( {
|
|
label: this.numItems,
|
|
flags: buttonFlags,
|
|
badgeIcon: config.badgeIcon,
|
|
// The following messages can be used here:
|
|
// tooltip-pt-notifications-alert
|
|
// tooltip-pt-notifications-message
|
|
title: mw.msg( 'tooltip-pt-notifications-' + this.type ),
|
|
href: config.href
|
|
} );
|
|
|
|
// Notifications widget
|
|
this.notificationsWidget = new mw.echo.ui.NotificationsWidget(
|
|
this.notificationsModel,
|
|
{
|
|
type: this.type,
|
|
$overlay: this.$menuOverlay,
|
|
markReadWhenSeen: this.markReadWhenSeen
|
|
}
|
|
);
|
|
|
|
// 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 ]
|
|
} );
|
|
$footer = $( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationBadgeButtonPopupWidget-footer' )
|
|
.append( footerButtonGroupWidget.$element );
|
|
|
|
this.popup = new OO.ui.PopupWidget( {
|
|
$content: this.notificationsWidget.$element,
|
|
$footer: $footer,
|
|
width: config.popupWidth || 500,
|
|
autoClose: true,
|
|
// 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-message-text-only
|
|
label: mw.msg( 'echo-notification-' + this.type + '-text-only' ),
|
|
classes: [ 'mw-echo-ui-notificationBadgeButtonPopupWidget-popup' ]
|
|
} );
|
|
// HACK: Add an icon to the popup head label
|
|
this.popupHeadIcon = new OO.ui.IconWidget();
|
|
this.popup.$head.prepend( this.popupHeadIcon.$element );
|
|
|
|
this.setPendingElement( this.popup.$head );
|
|
this.updateIcon( !!config.hasUnseen );
|
|
|
|
// 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.notificationsModel.connect( this, {
|
|
updateSeenTime: 'updateBadge',
|
|
add: 'updateBadge',
|
|
unseenChange: 'updateBadge',
|
|
unreadChange: 'updateBadge'
|
|
} );
|
|
this.popup.connect( this, { toggle: 'onPopupToggle' } );
|
|
this.badgeButton.connect( this, {
|
|
click: 'onBadgeButtonClick'
|
|
} );
|
|
|
|
this.$element
|
|
.prop( 'id', 'pt-notifications-' + this.type )
|
|
// 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-' + this.type
|
|
)
|
|
.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 */
|
|
|
|
/**
|
|
* Respond to badge button click
|
|
*/
|
|
mw.echo.ui.NotificationBadgeWidget.prototype.onBadgeButtonClick = function () {
|
|
if ( !this.popup.isVisible() ) {
|
|
// Force a new API request for notifications
|
|
this.notificationsModel.fetchNotifications( true );
|
|
}
|
|
|
|
this.popup.toggle();
|
|
};
|
|
|
|
/**
|
|
* Update the badge icon with the read/unread versions if they exist.
|
|
*
|
|
* @param {boolean} hasUnseen Widget has unseen notifications
|
|
*/
|
|
mw.echo.ui.NotificationBadgeWidget.prototype.updateIcon = function ( hasUnseen ) {
|
|
var icon = typeof this.badgeIcon === 'string' ?
|
|
this.badgeIcon :
|
|
this.badgeIcon[ hasUnseen ? 'unseen' : 'seen' ];
|
|
|
|
this.badgeButton.setIcon( icon );
|
|
this.popupHeadIcon.setIcon( icon );
|
|
};
|
|
|
|
/**
|
|
* Update the badge state and label based on changes to the model
|
|
*/
|
|
mw.echo.ui.NotificationBadgeWidget.prototype.updateBadge = function () {
|
|
var unseenCount = this.notificationsModel.getUnseenCount(),
|
|
unreadCount = this.notificationsModel.getUnreadCount(),
|
|
nonBundledUnreadCount = this.notificationsModel.getNonbundledUnreadCount(),
|
|
wgEchoMaxNotificationCount,
|
|
badgeLabel;
|
|
|
|
// Update numbers and seen/unseen state
|
|
// If the popup is open, only allow a "demotion" of the badge
|
|
// to grey; ignore change of color to 'unseen'
|
|
if ( this.popup.isVisible() ) {
|
|
if ( !unseenCount ) {
|
|
this.badgeButton.setFlags( { unseen: false } );
|
|
this.updateIcon( false );
|
|
}
|
|
} else {
|
|
this.badgeButton.setFlags( { unseen: !!unseenCount } );
|
|
this.updateIcon( !!unseenCount );
|
|
}
|
|
|
|
// Update badge count
|
|
if ( !this.markReadWhenSeen || !this.popup.isVisible() || unreadCount < this.currentUnreadCountInBadge ) {
|
|
wgEchoMaxNotificationCount = mw.config.get( 'wgEchoMaxNotificationCount' );
|
|
if ( unreadCount > wgEchoMaxNotificationCount ) {
|
|
badgeLabel = mw.message( 'echo-notification-count', wgEchoMaxNotificationCount ).text();
|
|
} else {
|
|
badgeLabel = mw.language.convertNumber( unreadCount );
|
|
}
|
|
this.badgeButton.setLabel( badgeLabel );
|
|
}
|
|
|
|
// Check if we need to display the 'mark all unread' button
|
|
this.markAllReadButton.toggle( !this.markReadWhenSeen && nonBundledUnreadCount > 0 );
|
|
this.currentUnreadCountInBadge = unreadCount;
|
|
};
|
|
|
|
/**
|
|
* Respond to 'mark all as read' button click
|
|
*/
|
|
mw.echo.ui.NotificationBadgeWidget.prototype.onMarkAllReadButtonClick = function () {
|
|
this.notificationsModel.markAllRead();
|
|
};
|
|
|
|
/**
|
|
* 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 ) {
|
|
// If the popup is closing, remove "initiallyUnseen" and leave
|
|
this.notificationsWidget.resetNotificationItems();
|
|
return;
|
|
}
|
|
|
|
// Log the click event
|
|
mw.echo.logger.logInteraction(
|
|
'ui-badge-link-click',
|
|
mw.echo.Logger.static.context.badge,
|
|
null,
|
|
this.type
|
|
);
|
|
|
|
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.notificationsModel.fetchNotifications()
|
|
.then( function () {
|
|
var i,
|
|
items = widget.notificationsWidget.getItems();
|
|
|
|
if ( widget.popup.isVisible() ) {
|
|
widget.popup.clip();
|
|
|
|
// Update seen time
|
|
widget.notificationsModel.updateSeenTime();
|
|
// Mark notifications as 'read' if markReadWhenSeen is set to true
|
|
if ( widget.markReadWhenSeen ) {
|
|
widget.notificationsModel.autoMarkAllRead();
|
|
}
|
|
|
|
// Log impressions
|
|
// TODO: Only log the impressions of notifications that
|
|
// are actually visible
|
|
for ( i = 0; i < items.length; i++ ) {
|
|
if ( items[ i ].getModel ) {
|
|
mw.echo.logger.logInteraction(
|
|
mw.echo.Logger.static.actions.notificationImpression,
|
|
mw.echo.Logger.static.context.popup,
|
|
items[ i ].getModel().getId(),
|
|
items[ i ].getModel().getCategory()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
widget.emit( 'finishLoading' );
|
|
} )
|
|
.always( function () {
|
|
// Pop pending
|
|
widget.popPending();
|
|
widget.promiseRunning = false;
|
|
} );
|
|
this.hasRunFirstTime = true;
|
|
};
|
|
|
|
/**
|
|
* Get the notifications model attached to this widget
|
|
*
|
|
* @return {mw.echo.dm.NotificationsModel} Notifications model
|
|
*/
|
|
mw.echo.ui.NotificationBadgeWidget.prototype.getModel = function () {
|
|
return this.notificationsModel;
|
|
};
|
|
|
|
} )( mediaWiki, jQuery );
|