mediawiki-extensions-Echo/modules/model/mw.echo.dm.NotificationsList.js

269 lines
6.9 KiB
JavaScript
Raw Normal View History

( function () {
/**
* Notifications list data structure.
*
* This contains the list of mw.echo.dm.NotificationItem items
* in the specified order and reflects when the list has changed.
*
* @class
* @extends mw.echo.dm.SortedList
*
* @constructor
* @param {Object} config Configuration options
* @param {Function} [config.sortingCallback] A function defining the sorting order
Fix fade-in/out animation in sorting The fade in/out animation is asynchronous. This means that if we are sorting multiple items one after the other, by the time the item faded out, it will be reinserted back into the wrong position, breaking the sorting. This also broke the promise of OO.SortedEmitterList whereby all its items are always in order. The way to fix this was to force a better synchronization with the item order while we hide and show the item in its new place. To do that, a new widget is created as a fake clone of the old one, in the original position of the old one. The original item is then reinserted (while hidden) to the proper location -- preserving order. The fake clone is then faded out, and the real item is then faded in. For this to work properly, the cloned item had to preserve some of the original item's information, like timestamp, foreigness and id. However, since both the real item and the fake new clone have the same details, the clone fakes its ID by adding a fraction to it - promising that the fallback in case of equal timestamps (which happens on the real and cloned items) will still resolve with some decision about the placement of the items rather than (falsely but understandably) decide they are both the same. Since this whole animation is somewhat of a hack, the list now has a configuration parameter to turn the animation on. The animation is on in the popups, but off in the special page. Bug: T141419 Change-Id: Ic7c35e5ddefc51bf7fde497eab36414b4dddcd9e
2016-07-29 23:35:29 +00:00
* of items in this list.
* @param {string} [config.title] An optional title for this notifications list
* @param {string} [config.name='local'] Symbolic name for this list
* @param {string} [config.source='local'] Symbolic name for the source of this list.
* This is used mainly for recognizing where API actions should be by the
* controller.
* @param {string} [config.sourceURL] The URL for the article base of the remote
* group or wiki
* @param {string} [config.timestamp=0] A timestamp representing the latest item in
* the list.
*/
mw.echo.dm.NotificationsList = function MwEchoDmNotificationsList( config ) {
config = config || {};
// Parent constructor
mw.echo.dm.NotificationsList.super.call( this );
this.name = config.name || 'local';
this.source = config.source || 'local';
this.sourceURL = config.sourceURL || '';
this.title = config.title || '';
this.fallbackTimestamp = config.timestamp || 0;
// Sorting callback
this.setSortingCallback( config.sortingCallback || ( ( a, b ) => {
if ( !a.isRead() && b.isRead() ) {
return -1; // Unread items are always above read items
} else if ( a.isRead() && !b.isRead() ) {
return 1;
} else if ( !a.isForeign() && b.isForeign() ) {
return -1;
} else if ( a.isForeign() && !b.isForeign() ) {
return 1;
}
// Reverse sorting
if ( b.getTimestamp() < a.getTimestamp() ) {
return -1;
} else if ( b.getTimestamp() > a.getTimestamp() ) {
return 1;
}
// Fallback on IDs
return b.getId() - a.getId();
} ) );
// Events
this.aggregate( { update: 'itemUpdate' } );
};
/* Initialization */
OO.inheritClass( mw.echo.dm.NotificationsList, mw.echo.dm.SortedList );
/* Events */
/**
* The list has been updated
*
* @event mw.echo.dm.NotificationsList#update
* @param {mw.echo.dm.NotificationItem[]} items Current items in the list
*/
/**
* An item in the list has been updated
*
* @event mw.echo.dm.NotificationsList#itemUpdate
* @param {mw.echo.dm.NotificationItem} item Item that has changed
*/
Relate read-state filter and mark read/unread action When we are viewing a certain read state filter ('read' or 'unread') the visibility of items should correspond to that state even when the user marks a specific item as read/unread. That means that the system should remove these items from view when the action is taken. In this commit: * The controller makes the judgment of whether to remove items when read/unread action is taken, based on whether a filter is set. * We clean up the terminology of discard - no more 'remove' - to make sure we have consistency in the code. * Related: The 'discard' event is now scoped within the hierarchy; meaning, lists emit 'discard' when an item is removed, grouplist emits 'discard' when a group is removed, and the manager emits 'discard' when an entire notification model is removed. This means we can actually have proper hierarchy and organization with a single event, and not worry about clashing between the intentional 'discard' action and the event 'remove' that is also used while resorting happens. * The model manager emits a discard event when a model is removed so that the general list can listen to the manager and remove an entire batch of items if needed. * The pagination model now updates the count for the current page rather than some vague notion of the last page. This is also updated when the controller removes items, so we can get an accurate count in the page for the number of notifications that are displayed. Bug: T136891 Change-Id: I247c618042ef256fadf09922f7b83bd1ad361f64
2016-07-14 00:03:57 +00:00
/**
* An item was discarded
*
* @event mw.echo.dm.NotificationsList#discard
* @param {mw.echo.dm.NotificationItem} item Item that was discarded
Relate read-state filter and mark read/unread action When we are viewing a certain read state filter ('read' or 'unread') the visibility of items should correspond to that state even when the user marks a specific item as read/unread. That means that the system should remove these items from view when the action is taken. In this commit: * The controller makes the judgment of whether to remove items when read/unread action is taken, based on whether a filter is set. * We clean up the terminology of discard - no more 'remove' - to make sure we have consistency in the code. * Related: The 'discard' event is now scoped within the hierarchy; meaning, lists emit 'discard' when an item is removed, grouplist emits 'discard' when a group is removed, and the manager emits 'discard' when an entire notification model is removed. This means we can actually have proper hierarchy and organization with a single event, and not worry about clashing between the intentional 'discard' action and the event 'remove' that is also used while resorting happens. * The model manager emits a discard event when a model is removed so that the general list can listen to the manager and remove an entire batch of items if needed. * The pagination model now updates the count for the current page rather than some vague notion of the last page. This is also updated when the controller removes items, so we can get an accurate count in the page for the number of notifications that are displayed. Bug: T136891 Change-Id: I247c618042ef256fadf09922f7b83bd1ad361f64
2016-07-14 00:03:57 +00:00
*/
/* Methods */
/**
* Set the items in this list
*
* @param {mw.echo.dm.NotificationItem[]} items Items to insert into the list
* @fires mw.echo.dm.NotificationsList#update
*/
mw.echo.dm.NotificationsList.prototype.setItems = function ( items ) {
this.clearItems();
this.addItems( items );
this.emit( 'update', this.getItems() );
};
/**
* Discard items from the list.
*
* This is a more precise operation than 'removeItems' because when
* the list is resorting the position of a single item, it removes
* the item and reinserts it, which makes the 'remove' event unhelpful
* to differentiate between actually discarding items, and only
* temporarily moving them.
*
* @param {mw.echo.dm.NotificationItem[]} items Items to insert into the list
* @fires mw.echo.dm.NotificationsList#discard
*/
mw.echo.dm.NotificationsList.prototype.discardItems = function ( items ) {
this.removeItems( items );
this.emit( 'discard', items );
};
/**
* Get an array of all items' IDs.
*
* @return {number[]} Item IDs
*/
mw.echo.dm.NotificationsList.prototype.getAllItemIds = function () {
const idArray = [],
items = this.getItems();
for ( let i = 0; i < items.length; i++ ) {
idArray.push( items[ i ].getId() );
}
return idArray;
};
/**
* Get an array of all items' IDs for a given type
*
* @param {string} type Notification type
* @return {number[]} Item IDs
*/
mw.echo.dm.NotificationsList.prototype.getAllItemIdsByType = function ( type ) {
const idArray = [],
items = this.getItems();
for ( let i = 0; i < items.length; i++ ) {
if ( items[ i ].getType() === type ) {
idArray.push( items[ i ].getId() );
}
}
return idArray;
};
/**
* Get the title associated with this list.
*
* @return {string} List title
*/
mw.echo.dm.NotificationsList.prototype.getTitle = function () {
return this.title;
};
/**
* Get the name associated with this list.
*
* @return {string} List name
*/
mw.echo.dm.NotificationsList.prototype.getName = function () {
return this.name;
};
/**
* Get the source associated with this list.
*
* @return {string} List source
*/
mw.echo.dm.NotificationsList.prototype.getSource = function () {
return this.source;
};
/**
* Get the source article url associated with this list.
*
* @return {string} List source article url
*/
mw.echo.dm.NotificationsList.prototype.getSourceURL = function () {
return this.sourceURL;
};
/**
* Get the timestamp of the list by taking the latest notification
* timestamp.
*
* @return {string} Latest timestamp
*/
mw.echo.dm.NotificationsList.prototype.getTimestamp = function () {
const items = this.getItems();
return (
// In the cases where we want a single timestamp for a
// group, the group is usually all unread, which makes
// the first item its newest
items.length > 0 ?
items[ 0 ].getTimestamp() :
this.fallbackTimestamp
);
};
/**
* Find all items that match the given IDs.
*
* @param {number[]} ids An array of item IDs
* @return {mw.echo.dm.NotificationItem[]} An array of matching items
*/
mw.echo.dm.NotificationsList.prototype.findByIds = function ( ids ) {
return this.getItems().filter( ( item ) => ids.indexOf( item.getId() ) !== -1 );
};
/**
* A general method to get the number of notifications in this list
*
* @return {number} Item count
*/
mw.echo.dm.NotificationsList.prototype.getCount = function () {
return this.getItemCount();
};
/**
* Check if there are unseen items in this list
*
* @return {boolean} There are unseen items in the list
*/
mw.echo.dm.NotificationsList.prototype.hasUnseen = function () {
const isItemUnseen = function ( item ) {
return !item.isSeen();
},
items = this.getItems();
return !!items.some( isItemUnseen );
};
/**
* Set all notifications to seen
*
* @param {string} timestamp New seen timestamp
*/
mw.echo.dm.NotificationsList.prototype.updateSeenState = function ( timestamp ) {
this.getItems().forEach( ( notification ) => {
notification.toggleSeen(
notification.isRead() || notification.getTimestamp() < timestamp
);
} );
};
/**
* @inheritdoc
*/
mw.echo.dm.NotificationsList.prototype.isGroup = function () {
return false;
};
mw.echo.dm.NotificationsList.prototype.isForeign = function () {
return this.getSource() !== 'local';
};
}() );