2015-08-13 00:54:16 +00:00
|
|
|
( function ( mw, $ ) {
|
|
|
|
/**
|
|
|
|
* Notification view model
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @mixins OO.EventEmitter
|
|
|
|
*
|
|
|
|
* @constructor
|
2016-01-15 22:11:33 +00:00
|
|
|
* @param {mw.echo.api.EchoApi} api Network handler
|
2016-03-15 21:13:19 +00:00
|
|
|
* @param {mw.echo.dm.UnreadNotificationCounter} unreadCounter Counter of unread notifications
|
2015-08-13 00:54:16 +00:00
|
|
|
* @param {Object} [config] Configuration object
|
2015-10-16 23:18:25 +00:00
|
|
|
* @cfg {string} [id] Model id, used to refer to the model specifically.
|
|
|
|
* Falls back to the model's unique source
|
|
|
|
* @cfg {string} [title=''] An optional title for the model. This is mostly used
|
|
|
|
* for nested bundled models inside group items.
|
2015-10-27 00:05:43 +00:00
|
|
|
* @cfg {string|string[]} [type='alert'] Notification type 'alert', 'message'
|
|
|
|
* or an array [ 'alert', 'message' ]
|
2015-11-11 23:22:36 +00:00
|
|
|
* @cfg {string} [source='local'] Model source, 'local' or some symbolic name identifying
|
|
|
|
* the source of the notification items for the network handler.
|
2015-08-13 00:54:16 +00:00
|
|
|
* @cfg {number} [limit=25] Notification limit
|
|
|
|
* @cfg {string} [userLang] User language
|
2016-02-27 05:43:46 +00:00
|
|
|
* @cfg {number} [timestamp] Timestamp (in MW format) to return from #getTimestamp when
|
|
|
|
* there are no items; use this if the timestamp is known ahead of time (before population).
|
2015-11-25 04:07:54 +00:00
|
|
|
* @cfg {boolean} [foreign] The model's source is foreign
|
2015-10-16 23:18:25 +00:00
|
|
|
* @cfg {boolean} [removeReadNotifications=false] Remove read notifications completely. This
|
|
|
|
* means the model will only contain unread notifications. This is useful for
|
|
|
|
* cross-wiki bundled notifications.
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
2016-03-15 21:13:19 +00:00
|
|
|
mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( api, unreadCounter, config ) {
|
2015-08-13 00:54:16 +00:00
|
|
|
config = config || {};
|
|
|
|
|
|
|
|
// Mixin constructor
|
|
|
|
OO.EventEmitter.call( this );
|
|
|
|
|
|
|
|
// Mixin constructor
|
2015-10-28 20:47:54 +00:00
|
|
|
mw.echo.dm.SortedList.call( this );
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2016-03-15 21:13:19 +00:00
|
|
|
this.unreadCounter = unreadCounter;
|
|
|
|
|
2015-08-13 00:54:16 +00:00
|
|
|
this.type = config.type || 'alert';
|
2015-11-11 23:22:36 +00:00
|
|
|
this.source = config.source || 'local';
|
2015-10-16 23:18:25 +00:00
|
|
|
this.id = config.id || this.source;
|
|
|
|
this.title = config.title || '';
|
2016-02-27 05:43:46 +00:00
|
|
|
this.fallbackTimestamp = config.timestamp;
|
2015-10-16 23:18:25 +00:00
|
|
|
|
|
|
|
this.markingAllAsRead = false;
|
2015-12-28 23:12:37 +00:00
|
|
|
this.autoMarkReadInProcess = false;
|
2016-03-31 07:53:07 +00:00
|
|
|
this.fetchingNotifications = false;
|
2015-12-28 23:12:37 +00:00
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
this.removeReadNotifications = !!config.removeReadNotifications;
|
2015-11-25 04:07:54 +00:00
|
|
|
this.foreign = !!config.foreign;
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2016-01-15 22:11:33 +00:00
|
|
|
this.api = api;
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2015-10-17 00:02:39 +00:00
|
|
|
this.seenTime = mw.config.get( 'wgEchoSeenTime' ) || {};
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2016-03-15 21:13:19 +00:00
|
|
|
// Store references to unseen notifications
|
2015-08-13 00:54:16 +00:00
|
|
|
this.unseenNotifications = new mw.echo.dm.NotificationList();
|
|
|
|
|
|
|
|
// Events
|
|
|
|
this.aggregate( {
|
|
|
|
seen: 'itemSeen',
|
2015-10-16 23:18:25 +00:00
|
|
|
read: 'itemRead',
|
2016-03-15 21:13:19 +00:00
|
|
|
empty: 'itemGroupEmpty'
|
2015-08-13 00:54:16 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
this.connect( this, {
|
|
|
|
itemSeen: 'onItemSeen',
|
2015-10-16 23:18:25 +00:00
|
|
|
itemRead: 'onItemRead',
|
|
|
|
itemGroupEmpty: 'onItemGroupEmpty'
|
2015-08-13 00:54:16 +00:00
|
|
|
} );
|
2015-10-28 20:47:54 +00:00
|
|
|
|
|
|
|
this.setSortingCallback( function ( a, b ) {
|
|
|
|
var diff;
|
|
|
|
|
|
|
|
if ( !a.isRead() && b.isRead() ) {
|
|
|
|
return -1; // Unread items are always above read items
|
|
|
|
} else if ( a.isRead() && !b.isRead() ) {
|
|
|
|
return 1;
|
2016-02-26 21:10:39 +00:00
|
|
|
} else if ( !a.isForeign() && b.isForeign() ) {
|
|
|
|
return -1;
|
|
|
|
} else if ( a.isForeign() && !b.isForeign() ) {
|
|
|
|
return 1;
|
2015-10-28 20:47:54 +00:00
|
|
|
} else {
|
|
|
|
// Reverse sorting
|
2015-10-16 23:18:25 +00:00
|
|
|
diff = Number( b.getTimestamp() ) - Number( a.getTimestamp() );
|
2015-10-28 20:47:54 +00:00
|
|
|
if ( diff !== 0 ) {
|
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback on IDs
|
|
|
|
return b.getId() - a.getId();
|
|
|
|
}
|
|
|
|
} );
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Initialization */
|
|
|
|
|
|
|
|
OO.initClass( mw.echo.dm.NotificationsModel );
|
|
|
|
OO.mixinClass( mw.echo.dm.NotificationsModel, OO.EventEmitter );
|
2015-10-28 20:47:54 +00:00
|
|
|
OO.mixinClass( mw.echo.dm.NotificationsModel, mw.echo.dm.SortedList );
|
2015-08-13 00:54:16 +00:00
|
|
|
|
|
|
|
/* Events */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event updateSeenTime
|
|
|
|
*
|
|
|
|
* Seen time has been updated
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event unseenChange
|
2015-11-03 22:51:20 +00:00
|
|
|
* @param {mw.echo.dm.NotificationItem} items An array of the unseen items
|
2015-08-13 00:54:16 +00:00
|
|
|
*
|
|
|
|
* Items' seen status has changed
|
|
|
|
*/
|
|
|
|
|
2015-09-17 20:47:23 +00:00
|
|
|
/**
|
|
|
|
* @event allRead
|
|
|
|
*
|
|
|
|
* All items are marked as read
|
|
|
|
*/
|
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
/**
|
|
|
|
* @event empty
|
|
|
|
*
|
|
|
|
* The model is empty
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event done
|
|
|
|
* @param {boolean} success The operation is successful
|
|
|
|
* @param {Object} result The result of the operation. For success, the
|
|
|
|
* result includes the ids of the items. For failures, the result
|
|
|
|
* includes the error code from the failed API request
|
|
|
|
* @param {string[]} [result.ids] An array of notification IDs that were
|
|
|
|
* fetched from the API. This only appears on success.
|
|
|
|
* @param {string} [result.errCode] The error code from the API.
|
|
|
|
* This only appears on failure.
|
|
|
|
* @param {Object} [result.errObj] The error object from the API.
|
|
|
|
* This only appears on failure.
|
|
|
|
*
|
|
|
|
* The process of fetching notifications from the API has finished
|
|
|
|
*/
|
|
|
|
|
2015-08-13 00:54:16 +00:00
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Respond to item seen state change
|
|
|
|
*
|
|
|
|
* @param {mw.echo.dm.NotificationItem} item Notification item
|
|
|
|
* @param {boolean} isSeen Notification is seen
|
|
|
|
* @fires unseenChange
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.onItemSeen = function ( item, isSeen ) {
|
|
|
|
var id = item && item.getId(),
|
|
|
|
unseenItem = id && this.unseenNotifications.getItemById( id );
|
|
|
|
|
|
|
|
if ( unseenItem ) {
|
|
|
|
if ( isSeen ) {
|
|
|
|
this.unseenNotifications.removeItems( [ unseenItem ] );
|
|
|
|
} else {
|
|
|
|
this.unseenNotifications.addItems( [ unseenItem ] );
|
|
|
|
}
|
|
|
|
this.emit( 'unseenChange', this.unseenNotifications.getItems() );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Respond to item read state change
|
|
|
|
*
|
|
|
|
* @param {mw.echo.dm.NotificationItem} item Notification item
|
|
|
|
* @param {boolean} isRead Notification is read
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.onItemRead = function ( item, isRead ) {
|
2016-03-15 21:13:19 +00:00
|
|
|
var model = this,
|
|
|
|
id = item && item.getId();
|
|
|
|
|
|
|
|
// The event 'itemRead' has different meanings depending on who fires it.
|
|
|
|
// When fired by NotificationItem, it means: "please call the API to mark me as read"
|
|
|
|
// When fired by NotificationsGroupItem, it means: "I've already called the API to do the work"
|
|
|
|
if ( item instanceof mw.echo.dm.NotificationGroupItem ) {
|
|
|
|
return;
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2015-10-28 20:47:54 +00:00
|
|
|
// Update unread status and emit events
|
2016-03-04 22:44:22 +00:00
|
|
|
if ( isRead ) {
|
|
|
|
// We are skipping "mark as read" when the operation is "mark all read"
|
|
|
|
// because the API takes a single request to mark all notifications
|
|
|
|
// as read, and we don't need to send multiple individual requests.
|
|
|
|
if ( !this.markingAllAsRead ) {
|
2016-03-15 21:13:19 +00:00
|
|
|
this.unreadCounter.estimateChange( -1 );
|
|
|
|
this.toggleItemsReadInApi( id, isRead ).then( function () {
|
|
|
|
model.unreadCounter.update();
|
|
|
|
} );
|
2015-08-13 00:54:16 +00:00
|
|
|
}
|
2016-03-04 22:44:22 +00:00
|
|
|
if ( this.removeReadNotifications ) {
|
|
|
|
// Remove this notification from the model
|
|
|
|
this.removeItems( [ item ] );
|
|
|
|
}
|
|
|
|
|
2016-03-15 21:13:19 +00:00
|
|
|
} else {
|
|
|
|
this.unreadCounter.estimateChange( 1 );
|
|
|
|
this.toggleItemsReadInApi( id, isRead ).then( function () {
|
|
|
|
model.unreadCounter.update();
|
|
|
|
} );
|
2015-09-17 20:47:23 +00:00
|
|
|
}
|
2015-10-01 17:56:40 +00:00
|
|
|
|
|
|
|
if ( !this.countUnreadTalkPageNotifications() ) {
|
|
|
|
this.emit( 'allTalkRead' );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
/**
|
|
|
|
* Respond to grouped item being empty
|
|
|
|
*
|
|
|
|
* @param {mw.echo.dm.NotificationItem} item Group item
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.onItemGroupEmpty = function ( item ) {
|
|
|
|
// TODO: When we have other types of bundles, we should check how to handle
|
|
|
|
// empty bundles (and bundles with only 1 item left)
|
|
|
|
// In this case, the notification is a "cross wiki" notification, which
|
|
|
|
// goes away when it is empty
|
|
|
|
this.removeItems( [ item ] );
|
|
|
|
};
|
|
|
|
|
2015-10-01 20:35:35 +00:00
|
|
|
/**
|
|
|
|
* Count the unread messages that originate from the user talk page.
|
|
|
|
*
|
|
|
|
* @return {number} Number of unread talk page messages
|
|
|
|
*/
|
2015-10-01 17:56:40 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.countUnreadTalkPageNotifications = function () {
|
|
|
|
var i, len,
|
|
|
|
talk = 0,
|
2016-03-15 21:13:19 +00:00
|
|
|
item,
|
|
|
|
items = this.getItems();
|
2015-10-01 17:56:40 +00:00
|
|
|
|
|
|
|
for ( i = 0, len = items.length; i < len; i++ ) {
|
2016-03-15 21:13:19 +00:00
|
|
|
item = items[ i ];
|
|
|
|
if ( !item.isRead() && item.getCategory() === 'edit-user-talk' ) {
|
2015-10-01 17:56:40 +00:00
|
|
|
talk++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return talk;
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the type of the notifications that this model deals with.
|
2015-10-27 00:05:43 +00:00
|
|
|
* Notifications types can be 'alert', 'message' or an array of both.
|
2015-08-13 00:54:16 +00:00
|
|
|
*
|
2015-10-27 00:05:43 +00:00
|
|
|
* @return {string|string[]} Notifications type
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getType = function () {
|
|
|
|
return this.type;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the counter of how many notifications are unseen
|
|
|
|
*
|
|
|
|
* @return {number} Number of unseen notifications
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getUnseenCount = function () {
|
2016-03-15 21:13:19 +00:00
|
|
|
return this.unseenNotifications.getItemCount();
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
/**
|
|
|
|
* Get the counter of how many regular, non bundled notifications are unread
|
|
|
|
*
|
|
|
|
* @return {number} Number of non bundled unread notifications
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getNonbundledUnreadCount = function () {
|
|
|
|
var i,
|
|
|
|
nonBundleItems = 0,
|
|
|
|
items = this.getItems();
|
|
|
|
|
|
|
|
for ( i = 0; i < items.length; i++ ) {
|
|
|
|
if (
|
|
|
|
!( items[ i ] instanceof mw.echo.dm.NotificationGroupItem ) &&
|
|
|
|
!items[ i ].isRead()
|
|
|
|
) {
|
|
|
|
nonBundleItems++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nonBundleItems;
|
|
|
|
};
|
|
|
|
|
2015-08-13 00:54:16 +00:00
|
|
|
/**
|
|
|
|
* Set the system seen time - the last time we've marked notification as seen
|
|
|
|
*
|
2015-09-03 21:49:50 +00:00
|
|
|
* @private
|
2016-01-20 23:05:21 +00:00
|
|
|
* @param {string} type Notification type; 'alert', 'message' or 'all'
|
|
|
|
* @param {string} time Mediawiki seen timestamp in Mediawiki timestamp format
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
2016-01-20 23:05:21 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.setSeenTime = function ( type, time ) {
|
|
|
|
var i, types;
|
|
|
|
|
|
|
|
// Normalize if using 'all'
|
|
|
|
types = type === 'all' ? [ 'alert', 'message' ] : [ type ];
|
2015-10-27 00:05:43 +00:00
|
|
|
|
|
|
|
for ( i = 0; i < type.length; i++ ) {
|
|
|
|
// Update all types
|
|
|
|
this.seenTime[ type[ i ] ] = time;
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the system seen time
|
|
|
|
*
|
2015-10-27 00:05:43 +00:00
|
|
|
* @param {string} [type] Notification type
|
2015-08-13 00:54:16 +00:00
|
|
|
* @return {string} Mediawiki seen timestamp in Mediawiki timestamp format
|
|
|
|
*/
|
2015-10-27 00:05:43 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.getSeenTime = function ( type ) {
|
2016-01-20 23:05:21 +00:00
|
|
|
var normalizedType;
|
|
|
|
|
|
|
|
type = type || this.type;
|
2015-10-27 00:05:43 +00:00
|
|
|
|
2016-01-20 23:05:21 +00:00
|
|
|
normalizedType = type === 'all' ?
|
|
|
|
[ 'alert', 'message' ] : [ type ];
|
2015-10-27 00:05:43 +00:00
|
|
|
|
2016-01-20 23:05:21 +00:00
|
|
|
return this.seenTime[ normalizedType[ 0 ] ];
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the seen timestamp
|
|
|
|
*
|
2015-10-27 00:05:43 +00:00
|
|
|
* @param {string|string[]} [type] Notification type
|
2015-08-13 00:54:16 +00:00
|
|
|
* @fires updateSeenTime
|
|
|
|
*/
|
2015-10-27 00:05:43 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.updateSeenTime = function ( type ) {
|
2016-01-20 23:05:21 +00:00
|
|
|
var i, len, types,
|
2015-09-03 21:49:50 +00:00
|
|
|
items = this.unseenNotifications.getItems();
|
|
|
|
|
2015-10-27 00:05:43 +00:00
|
|
|
type = type || this.type;
|
|
|
|
|
2016-01-20 23:05:21 +00:00
|
|
|
// If type is "all" or is not given, update both
|
|
|
|
types = type === 'all' ? [ 'alert', 'message' ] : [ type || this.type ];
|
|
|
|
|
2015-09-03 21:49:50 +00:00
|
|
|
// Update the notifications seen status
|
|
|
|
for ( i = 0, len = items.length; i < len; i++ ) {
|
2015-10-01 13:48:52 +00:00
|
|
|
items[ i ].toggleSeen( true );
|
2015-09-03 21:49:50 +00:00
|
|
|
}
|
|
|
|
this.emit( 'updateSeenTime' );
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2015-12-21 18:49:11 +00:00
|
|
|
// Only update seenTime in the API locally
|
2015-11-25 04:07:54 +00:00
|
|
|
if ( !this.isForeign() ) {
|
2016-01-20 23:05:21 +00:00
|
|
|
for ( i = 0; i < types.length; i++ ) {
|
|
|
|
this.api.updateSeenTime( this.getSource(), types[ i ] )
|
|
|
|
.then( this.setSeenTime.bind( this, types[ i ] ) );
|
|
|
|
}
|
2015-12-21 18:49:11 +00:00
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mark all notifications as read
|
|
|
|
*
|
|
|
|
* @return {jQuery.Promise} A promise that resolves when all notifications
|
|
|
|
* were marked as read.
|
2015-10-16 23:18:25 +00:00
|
|
|
* @fires empty
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.markAllRead = function () {
|
2016-03-15 21:13:19 +00:00
|
|
|
var model = this,
|
|
|
|
i, len, item,
|
|
|
|
items = this.getItems(),
|
2016-03-02 20:07:16 +00:00
|
|
|
itemIds = [],
|
2015-10-16 23:18:25 +00:00
|
|
|
length = items.length;
|
|
|
|
|
2015-12-28 23:12:37 +00:00
|
|
|
// Skip if this is an automatic "mark as read" and this model is
|
2016-01-15 22:11:33 +00:00
|
|
|
// foreign
|
|
|
|
if ( this.foreign && this.autoMarkReadInProcess ) {
|
2015-12-28 23:12:37 +00:00
|
|
|
return $.Deferred().resolve( 0 ).promise();
|
|
|
|
}
|
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
// In some cases our model is empty out of technicalities -- that is,
|
|
|
|
// we didn't fetch its items yet. In that case, when markAllRead is
|
|
|
|
// called, we should emit the empty event (that would have been
|
|
|
|
// emitted if there were items that were then marked as read and removed)
|
|
|
|
// and return a resolved promise
|
|
|
|
if ( length === 0 ) {
|
|
|
|
if ( this.removeReadNotifications ) {
|
|
|
|
this.emit( 'empty' );
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
return $.Deferred().resolve( 0 ).promise();
|
|
|
|
}
|
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
this.markingAllAsRead = true;
|
|
|
|
for ( i = 0, len = items.length; i < len; i++ ) {
|
2016-03-15 21:13:19 +00:00
|
|
|
item = items[ i ];
|
2016-01-15 22:11:33 +00:00
|
|
|
// Skip items that are foreign if we are in automatic 'mark all as read'
|
2016-05-06 15:19:08 +00:00
|
|
|
if ( !item.isRead() ) {
|
|
|
|
itemIds.push( item.getId() );
|
|
|
|
}
|
|
|
|
if ( !item.isForeign() ) {
|
2016-03-15 21:13:19 +00:00
|
|
|
item.toggleRead( true );
|
|
|
|
item.toggleSeen( true );
|
2015-10-16 23:18:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this.markingAllAsRead = false;
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2016-03-15 21:13:19 +00:00
|
|
|
if ( itemIds ) {
|
|
|
|
this.unreadCounter.estimateChange( -itemIds.length );
|
|
|
|
return this.api.markItemsRead( itemIds, this.getSource(), true ).then( function () {
|
|
|
|
model.unreadCounter.update();
|
|
|
|
} );
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
2015-12-28 23:12:37 +00:00
|
|
|
/**
|
|
|
|
* Trigger an automatic mark all notifications as read. It's important to mark
|
|
|
|
* this process as an automatic one, because there are several cases where we
|
|
|
|
* don't want to mark specific notifications as automatically read (like external
|
|
|
|
* group items)
|
|
|
|
*
|
|
|
|
* @fires empty
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.autoMarkAllRead = function () {
|
|
|
|
var model = this;
|
|
|
|
|
|
|
|
this.autoMarkReadInProcess = true;
|
2016-03-15 21:13:19 +00:00
|
|
|
this.markAllRead()
|
2015-12-28 23:12:37 +00:00
|
|
|
.then( function () {
|
|
|
|
model.autoMarkReadInProcess = false;
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2015-09-18 23:05:35 +00:00
|
|
|
/**
|
2016-01-15 22:11:33 +00:00
|
|
|
* Update the read status of a notification item, or a list of items, in the API
|
2015-09-18 23:05:35 +00:00
|
|
|
*
|
2016-01-15 22:11:33 +00:00
|
|
|
* @param {string|string[]} itemIds Item id or an array of item Ids
|
2015-09-18 23:05:35 +00:00
|
|
|
* @return {jQuery.Promise} A promise that resolves when the notifications
|
|
|
|
* were marked as read.
|
|
|
|
*/
|
2016-03-04 22:44:22 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.toggleItemsReadInApi = function ( itemIds, isRead ) {
|
|
|
|
itemIds = $.isArray( itemIds ) ? itemIds : [ itemIds ];
|
|
|
|
|
|
|
|
return this.api.markItemsRead( itemIds, this.getSource(), isRead );
|
2015-12-28 23:12:37 +00:00
|
|
|
};
|
|
|
|
|
2016-05-04 00:50:27 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.normalizeNotifData = function ( apiData ) {
|
|
|
|
var content = apiData[ '*' ] || {},
|
|
|
|
// Collect common data
|
|
|
|
notifData = {
|
|
|
|
read: !!apiData.read,
|
|
|
|
seen: !!apiData.read || apiData.timestamp.mw <= this.getSeenTime(),
|
|
|
|
timestamp: apiData.timestamp.utcmw,
|
|
|
|
category: apiData.category,
|
|
|
|
content: {
|
|
|
|
header: content.header,
|
|
|
|
body: content.body
|
|
|
|
},
|
|
|
|
iconURL: content.iconUrl,
|
|
|
|
iconType: content.icon,
|
|
|
|
type: this.getType(),
|
|
|
|
foreign: this.isForeign(),
|
|
|
|
source: this.getSource(),
|
|
|
|
primaryUrl: OO.getProp( content.links, 'primary', 'url' ),
|
|
|
|
secondaryUrls: OO.getProp( content.links, 'secondary' ) || []
|
|
|
|
};
|
|
|
|
|
|
|
|
if ( apiData.type === 'foreign' ) {
|
|
|
|
notifData = $.extend( notifData, {
|
|
|
|
// This should probably be separated by bundled
|
|
|
|
// type. Some types don't have read messages, but
|
|
|
|
// some do
|
|
|
|
removeReadNotifications: true,
|
|
|
|
// Override the foreign flag to 'true' for cross-wiki
|
|
|
|
// notifications.
|
|
|
|
// For bundles that are not foreign (like regular
|
|
|
|
// bundles of notifications) this flag should be false
|
|
|
|
foreign: true,
|
|
|
|
type: apiData.section,
|
|
|
|
count: apiData.count
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
return notifData;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process notifications list API data.
|
|
|
|
*
|
|
|
|
* @param {Object[]} notifList Notifications list API data
|
|
|
|
* @return {number[]} Array of notification IDs
|
|
|
|
* @fires done
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.processAPIData = function ( notifList ) {
|
|
|
|
var i, notifData,
|
|
|
|
notificationModel,
|
|
|
|
newNotifData = {},
|
|
|
|
sources = {},
|
|
|
|
items = [],
|
|
|
|
idArray = [];
|
|
|
|
|
|
|
|
notifList = notifList || {};
|
|
|
|
|
|
|
|
for ( i = 0; i < notifList.length; i++ ) {
|
|
|
|
notifData = notifList[ i ];
|
|
|
|
|
|
|
|
newNotifData = this.normalizeNotifData( notifData );
|
|
|
|
|
|
|
|
if ( notifData.type === 'foreign' ) {
|
|
|
|
// Register sources
|
|
|
|
sources = notifData.sources;
|
|
|
|
this.api.registerForeignSources( sources );
|
|
|
|
|
|
|
|
// Create model
|
|
|
|
notificationModel = new mw.echo.dm.NotificationGroupItem(
|
|
|
|
this.api,
|
|
|
|
this.unreadCounter,
|
|
|
|
sources,
|
|
|
|
notifData.id,
|
|
|
|
newNotifData
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
notificationModel = new mw.echo.dm.NotificationItem(
|
|
|
|
notifData.id,
|
|
|
|
newNotifData
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
idArray.push( notifData.id );
|
|
|
|
items.push( notificationModel );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty current items
|
|
|
|
// HACK: We're turning on a 'fetchingNotifications' flag
|
|
|
|
// so the x-wiki "empty" event is suppressed while
|
|
|
|
// we clear items just to fill them back up.
|
|
|
|
// Otherwise, the x-wiki notification bundle will be
|
|
|
|
// removed from the general list before it is refilled.
|
|
|
|
this.fetchingNotifications = true;
|
|
|
|
this.clearItems();
|
|
|
|
this.fetchingNotifications = false;
|
|
|
|
// Add again to the model
|
|
|
|
this.addItems( items, 0 );
|
|
|
|
|
|
|
|
this.emit( 'done', true, { ids: idArray } );
|
|
|
|
return idArray;
|
|
|
|
};
|
2015-08-13 00:54:16 +00:00
|
|
|
/**
|
|
|
|
* Fetch notifications from the API and update the notifications list.
|
|
|
|
*
|
2016-01-15 22:11:33 +00:00
|
|
|
* @param {boolean} [isForced] Force a renewed fetching promise. If set to false, the
|
|
|
|
* model will request the stored/cached fetching promise from the API. A 'true' value
|
|
|
|
* will force the API to re-request that information from the server and update the
|
|
|
|
* notifications.
|
2015-12-28 23:12:37 +00:00
|
|
|
* @return {jQuery.Promise} A promise that resolves with an array of notification IDs
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
2016-01-15 22:11:33 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.fetchNotifications = function ( isForced ) {
|
2015-10-17 00:02:39 +00:00
|
|
|
var model = this;
|
2015-08-13 00:54:16 +00:00
|
|
|
|
2016-03-15 21:13:19 +00:00
|
|
|
if ( !this.isForeign() ) {
|
|
|
|
this.unreadCounter.update();
|
|
|
|
}
|
|
|
|
|
2015-09-16 20:42:17 +00:00
|
|
|
// Rebuild the notifications promise either when it is null or when
|
|
|
|
// it exists in a failed state
|
2016-01-15 22:11:33 +00:00
|
|
|
return this.api.fetchNotifications( this.getType(), this.getSource(), !!isForced )
|
2015-10-16 23:18:25 +00:00
|
|
|
.then(
|
|
|
|
// Success
|
2016-01-15 22:11:33 +00:00
|
|
|
function ( data ) {
|
2016-05-04 00:50:27 +00:00
|
|
|
model.processAPIData( OO.getProp( data, 'list' ) );
|
2015-10-16 23:18:25 +00:00
|
|
|
},
|
|
|
|
// Failure
|
2016-05-04 00:50:27 +00:00
|
|
|
this.handleApiFetchError.bind( this )
|
2015-10-16 23:18:25 +00:00
|
|
|
);
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
2016-05-04 00:50:27 +00:00
|
|
|
/**
|
|
|
|
* Handle API errors on fetching operations.
|
|
|
|
*
|
|
|
|
* @param {string} errCode Error code
|
|
|
|
* @param {Object} errObj Error object
|
|
|
|
* @fires done
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.handleApiFetchError = function ( errCode, errObj ) {
|
|
|
|
// TODO: The 'analysis' of which error we are working with should
|
|
|
|
// be in the network layer of Echo's frontend code
|
|
|
|
this.emit(
|
|
|
|
'done',
|
|
|
|
false,
|
|
|
|
{
|
|
|
|
errCode: errCode,
|
|
|
|
errInfo: errCode === 'http' ?
|
|
|
|
mw.msg( 'echo-api-failure-cross-wiki' ) :
|
|
|
|
OO.getProp( errObj, 'error', 'info' )
|
|
|
|
} );
|
|
|
|
};
|
2015-08-13 00:54:16 +00:00
|
|
|
/**
|
2016-03-15 21:13:19 +00:00
|
|
|
* Update the unseen tracking lists when we add items
|
2015-08-13 00:54:16 +00:00
|
|
|
*
|
|
|
|
* @param {mw.echo.dm.NotificationItem[]} items Items to add
|
|
|
|
*/
|
2015-10-28 20:47:54 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.addItems = function ( items ) {
|
2015-08-13 00:54:16 +00:00
|
|
|
var i, len;
|
|
|
|
|
|
|
|
for ( i = 0, len = items.length; i < len; i++ ) {
|
2015-10-01 13:48:52 +00:00
|
|
|
if ( !items[ i ].isSeen() ) {
|
|
|
|
this.unseenNotifications.addItems( [ items[ i ] ] );
|
2015-08-13 00:54:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Parent
|
2015-10-28 20:47:54 +00:00
|
|
|
mw.echo.dm.SortedList.prototype.addItems.call( this, items );
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-03-15 21:13:19 +00:00
|
|
|
* Update the unseen tracking lists when we remove items
|
2015-08-13 00:54:16 +00:00
|
|
|
*
|
|
|
|
* @param {mw.echo.dm.NotificationItem[]} items Items to remove
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.removeItems = function ( items ) {
|
|
|
|
var i, len;
|
|
|
|
|
|
|
|
for ( i = 0, len = items.length; i < len; i++ ) {
|
2015-10-01 13:48:52 +00:00
|
|
|
this.unseenNotifications.removeItems( [ items[ i ] ] );
|
2015-08-13 00:54:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parent
|
2015-10-28 20:47:54 +00:00
|
|
|
mw.echo.dm.SortedList.prototype.removeItems.call( this, items );
|
2015-10-16 23:18:25 +00:00
|
|
|
|
2016-03-31 07:53:07 +00:00
|
|
|
if ( this.isEmpty() && !this.fetchingNotifications ) {
|
2015-10-16 23:18:25 +00:00
|
|
|
this.emit( 'empty' );
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-03-15 21:13:19 +00:00
|
|
|
* Update the unseen tracking lists when we clear items
|
2015-08-13 00:54:16 +00:00
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.clearItems = function () {
|
|
|
|
this.unseenNotifications.clearItems();
|
|
|
|
|
|
|
|
// Parent
|
2015-10-28 20:47:54 +00:00
|
|
|
mw.echo.dm.SortedList.prototype.clearItems.call( this );
|
2016-03-31 07:53:07 +00:00
|
|
|
|
|
|
|
if ( !this.fetchingNotifications ) {
|
|
|
|
this.emit( 'empty' );
|
|
|
|
}
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Query the API for unread count of the notifications in this model
|
|
|
|
*
|
|
|
|
* @return {jQuery.Promise} jQuery promise that's resolved when the unread count is fetched
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi = function () {
|
2016-01-15 22:11:33 +00:00
|
|
|
return this.api.fetchUnreadCount( this.getSource(), this.getType() );
|
2015-10-17 00:02:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether the model has an api error state flagged
|
|
|
|
*
|
|
|
|
* @return {boolean} The model is in api error state
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.isFetchingErrorState = function () {
|
2016-01-15 22:11:33 +00:00
|
|
|
return this.api.isFetchingErrorState( this.getSource(), this.getType() );
|
2015-10-17 00:02:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the fetch notifications promise
|
2016-03-07 13:29:15 +00:00
|
|
|
*
|
2015-10-17 00:02:39 +00:00
|
|
|
* @return {jQuery.Promise} Promise that is resolved when notifications were
|
|
|
|
* fetched from the API.
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise = function () {
|
2016-01-15 22:11:33 +00:00
|
|
|
return this.api.getFetchNotificationPromise( this.getSource(), this.getType() );
|
2015-08-13 00:54:16 +00:00
|
|
|
};
|
2015-11-11 23:22:36 +00:00
|
|
|
|
2015-10-16 23:18:25 +00:00
|
|
|
/**
|
|
|
|
* Get the timestamp of the latest unread item
|
|
|
|
*
|
2016-05-11 20:33:17 +00:00
|
|
|
* @return {mw.echo.api.APIHandler} API handler
|
2015-10-16 23:18:25 +00:00
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getTimestamp = function () {
|
|
|
|
var items = this.getItems();
|
|
|
|
|
|
|
|
// This is a sorted list, so the top (first) item is also the 'latest'
|
|
|
|
// item for this purpose.
|
2016-02-27 05:43:46 +00:00
|
|
|
return ( items[ 0 ] && items[ 0 ].getTimestamp() ) || this.fallbackTimestamp;
|
2015-10-16 23:18:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the source this model is associated with
|
|
|
|
*
|
|
|
|
* @return {string} Symbolic name for the APIHandler source
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getSource = function () {
|
|
|
|
return this.source;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the title of this model
|
|
|
|
*
|
|
|
|
* @return {string} Title
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getTitle = function () {
|
|
|
|
return this.title;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the id of this model
|
|
|
|
*
|
|
|
|
* @return {string} id
|
|
|
|
*/
|
|
|
|
mw.echo.dm.NotificationsModel.prototype.getId = function () {
|
|
|
|
return this.id;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-11-25 04:07:54 +00:00
|
|
|
* This model is foreign
|
2015-10-16 23:18:25 +00:00
|
|
|
*
|
2015-11-25 04:07:54 +00:00
|
|
|
* @return {boolean} Model is foreign
|
2015-10-16 23:18:25 +00:00
|
|
|
*/
|
2015-11-25 04:07:54 +00:00
|
|
|
mw.echo.dm.NotificationsModel.prototype.isForeign = function () {
|
|
|
|
return this.foreign;
|
2015-10-16 23:18:25 +00:00
|
|
|
};
|
|
|
|
|
2015-08-13 00:54:16 +00:00
|
|
|
} )( mediaWiki, jQuery );
|