mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Echo
synced 2024-11-25 00:05:29 +00:00
480ce92bd8
Allow extensions to add dynamic actions that perform some API request and display a confirmation message. Bug: T132975 Change-Id: Ib16d57c3f1a11a9749564c6e2112bf1ca32c55e8
418 lines
13 KiB
JavaScript
418 lines
13 KiB
JavaScript
( function ( mw, $ ) {
|
|
/*global moment:false */
|
|
/**
|
|
* A base widget for displaying notification items.
|
|
*
|
|
* @class
|
|
* @abstract
|
|
* @extends OO.ui.Widget
|
|
*
|
|
* @constructor
|
|
* @param {mw.echo.Controller} controller Echo controller
|
|
* @param {mw.echo.dm.NotificationItem} model Notification item model
|
|
* @param {Object} [config] Configuration options
|
|
* @cfg {jQuery} [$overlay] A jQuery element functioning as an overlay
|
|
* for popups.
|
|
* @cfg {boolean} [bundle=false] This notification item is part of a bundle.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget = function MwEchoUiNotificationItemWidget( controller, model, config ) {
|
|
var i, secondaryUrls, urlObj, linkButton, $icon, isOutsideMenu, echoMoment,
|
|
outsideMenuItemCounter = 0,
|
|
$message = $( '<div>' ).addClass( 'mw-echo-ui-notificationItemWidget-content-message' );
|
|
|
|
config = config || {};
|
|
|
|
// Parent
|
|
mw.echo.ui.NotificationItemWidget.parent.call( this, $.extend( { data: model.getId() }, config ) );
|
|
|
|
this.controller = controller;
|
|
this.model = model;
|
|
|
|
this.$overlay = config.$overlay || this.$element;
|
|
this.bundle = !!config.bundle;
|
|
|
|
this.$content = $( '<div>' ).addClass( 'mw-echo-ui-notificationItemWidget-content' );
|
|
this.$actions = $( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-actions' );
|
|
|
|
// Mark as read
|
|
this.markAsReadButton = new mw.echo.ui.ToggleReadCircleButtonWidget( {
|
|
framed: false,
|
|
title: mw.msg( 'echo-notification-markasread-tooltip' ),
|
|
classes: [ 'mw-echo-ui-notificationItemWidget-markAsReadButton' ],
|
|
markAsRead: !this.model.isRead()
|
|
} );
|
|
|
|
// Icon
|
|
if ( this.model.getIconURL() ) {
|
|
$icon = $( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-icon' )
|
|
.append( $( '<img>' ).attr( 'src', this.model.getIconURL() ) );
|
|
}
|
|
|
|
// Content
|
|
$message.append(
|
|
$( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-message-header-wrapper' )
|
|
.append(
|
|
$( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-message-header' )
|
|
.append( this.model.getContentHeader() )
|
|
)
|
|
);
|
|
if ( !this.bundle && this.model.getContentBody() ) {
|
|
$message.append(
|
|
$( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-message-body' )
|
|
.append( this.model.getContentBody() )
|
|
// dir=auto has a similar effect to wrapping the content in <bdi>, but
|
|
// makes text-overflow: ellipsis; behave less strangely
|
|
.attr( 'dir', 'auto' )
|
|
);
|
|
}
|
|
|
|
// Actions menu
|
|
this.actionsButtonSelectWidget = new OO.ui.ButtonSelectWidget( {
|
|
classes: [ 'mw-echo-ui-notificationItemWidget-content-actions-buttons' ]
|
|
} );
|
|
|
|
// Popup menu
|
|
this.menuPopupButtonWidget = new mw.echo.ui.ActionMenuPopupWidget( {
|
|
framed: false,
|
|
icon: 'ellipsis',
|
|
$overlay: this.$overlay,
|
|
menuWidth: 200,
|
|
title: mw.msg( 'echo-notification-more-options-tooltip' ),
|
|
classes: [ 'mw-echo-ui-notificationItemWidget-content-actions-menu' ]
|
|
} );
|
|
|
|
// Timestamp
|
|
// We want to use extra-short timestamp strings; we change the locale
|
|
// to our echo-defined one and use that instead of the normal moment locale
|
|
echoMoment = moment.utc( this.model.getTimestamp() );
|
|
echoMoment.locale( 'echo-shortRelativeTime' );
|
|
echoMoment.local();
|
|
|
|
this.timestampWidget = new OO.ui.LabelWidget( {
|
|
classes: [ 'mw-echo-ui-notificationItemWidget-content-actions-timestamp' ],
|
|
// Get the time 'fromNow' without the suffix 'ago'
|
|
label: echoMoment.fromNow( true )
|
|
} );
|
|
|
|
// Build the actions line
|
|
if ( this.bundle ) {
|
|
// In a bundled item, the timestamp should go before the menu
|
|
this.$actions.append(
|
|
$( '<div>' )
|
|
// We are wrapping the actions in a 'row' div so that the
|
|
// internal pieces are also a table layout
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-actions-row' )
|
|
.append(
|
|
this.actionsButtonSelectWidget.$element,
|
|
this.timestampWidget.$element,
|
|
this.menuPopupButtonWidget.$element
|
|
)
|
|
);
|
|
} else {
|
|
this.$actions.append(
|
|
this.actionsButtonSelectWidget.$element,
|
|
this.menuPopupButtonWidget.$element,
|
|
this.timestampWidget.$element
|
|
);
|
|
}
|
|
|
|
// Actions
|
|
secondaryUrls = this.model.getSecondaryUrls();
|
|
for ( i = 0; i < secondaryUrls.length; i++ ) {
|
|
urlObj = secondaryUrls[ i ];
|
|
|
|
// Items are placed outside the dotdotdot menu if they are
|
|
// prioritized explicitly, *except* for items inside a bundle
|
|
// (where all actions are inside the menu) or there are more than
|
|
// two prioritized actions (all others go into the menu)
|
|
isOutsideMenu = !this.bundle && urlObj.prioritized !== undefined && outsideMenuItemCounter < mw.echo.config.maxPrioritizedActions;
|
|
|
|
linkButton = new mw.echo.ui.MenuItemWidget( {
|
|
type: urlObj.type,
|
|
actionData: urlObj.data,
|
|
icon: urlObj.icon || 'next',
|
|
label: urlObj.label,
|
|
tooltip: urlObj.tooltip,
|
|
description: urlObj.description,
|
|
url: urlObj.url,
|
|
prioritized: isOutsideMenu
|
|
} );
|
|
|
|
// Limit to 2 items outside the menu
|
|
if ( isOutsideMenu ) {
|
|
this.actionsButtonSelectWidget.addItems( [ linkButton ] );
|
|
outsideMenuItemCounter++;
|
|
} else {
|
|
this.menuPopupButtonWidget.getMenu().addItems( [ linkButton ] );
|
|
}
|
|
}
|
|
|
|
if ( this.bundle ) {
|
|
// In a bundle, we have table layout, so the icon is
|
|
// inserted into the content, and the 'mark as read'
|
|
// button is not floating, and should be at the end
|
|
this.$content.append(
|
|
$icon,
|
|
$message,
|
|
this.$actions,
|
|
this.markAsReadButton.$element
|
|
);
|
|
this.$element.append( this.$content );
|
|
} else {
|
|
this.$content.append(
|
|
this.markAsReadButton.$element,
|
|
$message,
|
|
$( '<div>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-content-table' )
|
|
.append( this.$actions )
|
|
);
|
|
this.$element.append( $icon, this.$content );
|
|
}
|
|
|
|
// Events
|
|
this.menuPopupButtonWidget.getMenu().connect( this, { choose: 'onPopupButtonWidgetChoose' } );
|
|
this.markAsReadButton.connect( this, { click: 'onMarkAsReadButtonClick' } );
|
|
|
|
this.$element
|
|
.addClass( 'mw-echo-ui-notificationItemWidget' )
|
|
.toggleClass( 'mw-echo-ui-notificationItemWidget-initiallyUnseen', !this.model.isSeen() && !this.bundle )
|
|
.toggleClass( 'mw-echo-ui-notificationItemWidget-bundled', this.bundle );
|
|
|
|
// Wrap the entire item with primary url
|
|
if ( this.model.getPrimaryUrl() ) {
|
|
this.$element.contents()
|
|
.wrapAll(
|
|
// HACK: Wrap the entire item with a link that takes
|
|
// the user to the primary url. This is not perfect,
|
|
// but it makes the behavior native to the browser rather
|
|
// than us listening to click events and opening new
|
|
// windows.
|
|
$( '<a>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-linkWrapper' )
|
|
.attr( 'href', this.model.getPrimaryUrl() )
|
|
.on( 'click', this.onPrimaryLinkClick.bind( this ) )
|
|
);
|
|
}
|
|
};
|
|
|
|
OO.inheritClass( mw.echo.ui.NotificationItemWidget, OO.ui.Widget );
|
|
|
|
/**
|
|
* Respond to primary link click.
|
|
* Override this in the descendents.
|
|
*
|
|
* @return {boolean} true
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.onPrimaryLinkClick = function () {
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Manage a click on a dynamic secondary link.
|
|
* We can't know what the link intends us to do in the API, so we trust the 'apiParams'
|
|
* to tell the controller. When the link is clicked, we will pass the information on
|
|
* to the controller, which will manage whatever promise and action is needed.
|
|
*
|
|
* NOTE: The messages are parsed as HTML. If user-input is expected
|
|
* please make sure to properly escape it.
|
|
*
|
|
* @param {mw.echo.ui.MenuItemWidget} item The selected item
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.onPopupButtonWidgetChoose = function ( item ) {
|
|
var actionData = item && item.getActionData(),
|
|
messages = item && item.getConfirmationMessages(),
|
|
widget = this;
|
|
|
|
// Send to controller
|
|
item.pushPending();
|
|
this.controller.performDynamicAction( actionData, this.getModel().getSource() )
|
|
.then( function () {
|
|
var $title = $( '<p>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-notify-title' )
|
|
.append( $.parseHTML( messages.title ) ),
|
|
$description = $( '<p>' )
|
|
.addClass( 'mw-echo-ui-notificationItemWidget-notify-description' )
|
|
.append( $.parseHTML( messages.description ) ),
|
|
$confirmation;
|
|
|
|
// Get rid of the button
|
|
item.disconnect( this );
|
|
if ( item.isPrioritized() ) {
|
|
widget.actionsButtonSelectWidget.removeItems( [ item ] );
|
|
} else {
|
|
// It's inside the popup menu
|
|
widget.menuPopupButtonWidget.getMenu().removeItems( [ item ] );
|
|
}
|
|
|
|
// Make sure to hide either piece if it is empty
|
|
$title.toggle( !!$title.text() );
|
|
$description.toggle( !!$description.text() );
|
|
|
|
// Display confirmation
|
|
$confirmation = $( '<div>' )
|
|
.append( $title, $description );
|
|
|
|
// Send to mw.notify
|
|
mw.notify( $confirmation );
|
|
} );
|
|
|
|
// Prevent the click propagation
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Respond to mark as read button click
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.onMarkAsReadButtonClick = function () {
|
|
// If we're marking read or unread, the notification was already seen.
|
|
// Remove the animation class
|
|
this.$element.removeClass( 'mw-echo-ui-notificationItemWidget-initiallyUnseen' );
|
|
this.markRead( !this.model.isRead() );
|
|
// Prevent propogation in case there's a link wrapping the content
|
|
// and the mark as read/unread button
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Mark this notification as read
|
|
*
|
|
* @method
|
|
* @abstract
|
|
* @param {boolean} [isRead=true] Notification is marked as read
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.markRead = null;
|
|
|
|
/**
|
|
* Get the notification link
|
|
*
|
|
* @return {string} Notification link
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.getPrimaryUrl = function () {
|
|
return this.model.getPrimaryUrl();
|
|
};
|
|
|
|
/**
|
|
* Get the item id
|
|
*
|
|
* @return {number} Notification id
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.getTimestamp = function () {
|
|
return this.model.getTimestamp();
|
|
};
|
|
|
|
/**
|
|
* Get the notification Id
|
|
*
|
|
* @return {number} Notification id
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.getId = function () {
|
|
return this.model.getId();
|
|
};
|
|
|
|
/**
|
|
* Check whether this item is seen.
|
|
*
|
|
* @return {boolean} Item is seen
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.isSeen = function () {
|
|
return this.model.isSeen();
|
|
};
|
|
|
|
/**
|
|
* Check whether this item is read.
|
|
*
|
|
* @return {boolean} Item is read
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.isRead = function () {
|
|
return this.model.isRead();
|
|
};
|
|
|
|
/**
|
|
* Check whether this item is foreign.
|
|
*
|
|
* @return {boolean} Item is foreign
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.isForeign = function () {
|
|
return this.model.isForeign();
|
|
};
|
|
|
|
/**
|
|
* Toggle the function of the 'mark as read' buttons from 'mark as read' to 'mark as unread'
|
|
* and vice versa.
|
|
*
|
|
* @param {boolean} [showMarkAsRead] Show the 'mark as read' buttons
|
|
* - "false" means that the item is marked as read, whereby we show the user 'mark unread'
|
|
* buttons.
|
|
* - "true" means that the item is marked as unread and we show the user 'mark as read'
|
|
* buttons
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.toggleMarkAsReadButtons = function ( showMarkAsRead ) {
|
|
showMarkAsRead = showMarkAsRead !== undefined ? showMarkAsRead : !this.model.isRead();
|
|
|
|
this.markAsReadButton.toggleState( showMarkAsRead );
|
|
this.menuPopupButtonWidget.toggle( !this.menuPopupButtonWidget.getMenu().isEmpty() );
|
|
};
|
|
|
|
/**
|
|
* Toggle the read state of the widget
|
|
*
|
|
* @param {boolean} [read] The current read state. If not given, the state will
|
|
* become the opposite of its current state.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.toggleRead = function ( read ) {
|
|
this.read = read !== undefined ? read : !this.read;
|
|
|
|
this.$element.toggleClass( 'mw-echo-ui-notificationItemWidget-unread', !this.read );
|
|
this.toggleMarkAsReadButtons( !this.read );
|
|
};
|
|
|
|
/**
|
|
* Toggle the seen state of the widget
|
|
*
|
|
* @param {boolean} [seen] The current seen state. If not given, the state will
|
|
* become the opposite of its current state.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.toggleSeen = function ( seen ) {
|
|
this.seen = seen !== undefined ? seen : !this.seen;
|
|
|
|
this.$element
|
|
.toggleClass( 'mw-echo-ui-notificationItemWidget-unseen', !this.seen );
|
|
};
|
|
|
|
/**
|
|
* Get the model associated with this widget.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.getModel = function () {
|
|
return this.model;
|
|
};
|
|
|
|
/**
|
|
* Disconnect events when widget is destroyed.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.destroy = function () {
|
|
this.model.disconnect( this );
|
|
};
|
|
|
|
/**
|
|
* Remove the 'initiallyUnseen' class, which was only used for the
|
|
* unseen animation when the user has first seen it.
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.resetInitiallyUnseen = function () {
|
|
this.$element.removeClass( 'mw-echo-ui-notificationItemWidget-initiallyUnseen' );
|
|
};
|
|
|
|
/**
|
|
* Declares whether this widget is a cloned fake.
|
|
*
|
|
* @return {boolean} false
|
|
*/
|
|
mw.echo.ui.NotificationItemWidget.prototype.isFake = function () {
|
|
return false;
|
|
};
|
|
} )( mediaWiki, jQuery );
|