mediawiki-extensions-Echo/modules/viewmodel/mw.echo.dm.NotificationsModel.js
Moriel Schottlender c5905962ab Only load ext.echo.ui if the user clicks the echo badge
There is no need to load the entire of Echo's ui module (especially
since that includes ooui widgets and their styles) on every page load.
There's only need to load the entire module if and when a user clicks
the Echo notification badge.

Also, make the echo.dm model accept an external fetchNotifications
promise so we can send the API request alongside loading the echo UI
and "feed" it into the DM for processing.

CSS adjusted to make the "jump" between the nojs and the js buttons
seem less jumpy.

Bug: T112401
Change-Id: I516e655ffd198511d694489a0702c5c713a5fd68
2015-09-15 17:11:46 -07:00

386 lines
11 KiB
JavaScript

( function ( mw, $ ) {
/**
* Notification view model
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration object
* @cfg {string} [type='alert'] Notification type 'alert', 'message' or 'all'
* @cfg {number} [limit=25] Notification limit
* @cfg {string} [userLang] User language
*/
mw.echo.dm.NotificationsModel = function MwEchoDmNotificationsModel( config ) {
config = config || {};
// Mixin constructor
OO.EventEmitter.call( this );
// Mixin constructor
mw.echo.dm.List.call( this );
this.type = config.type || 'alert';
this.limit = config.limit || 25;
this.userLang = config.userLang || 'en';
this.api = new mw.Api( { ajax: { cache: false } } );
this.fetchNotificationsPromise = null;
this.seenTime = mw.config.get( 'wgEchoSeenTime' );
// Store references to unseen and unread notifications
this.unseenNotifications = new mw.echo.dm.NotificationList();
this.unreadNotifications = new mw.echo.dm.NotificationList();
// Events
this.aggregate( {
seen: 'itemSeen',
read: 'itemRead'
} );
this.connect( this, {
itemSeen: 'onItemSeen',
itemRead: 'onItemRead'
} );
};
/* Initialization */
OO.initClass( mw.echo.dm.NotificationsModel );
OO.mixinClass( mw.echo.dm.NotificationsModel, OO.EventEmitter );
OO.mixinClass( mw.echo.dm.NotificationsModel, mw.echo.dm.List );
/* Events */
/**
* @event updateSeenTime
*
* Seen time has been updated
*/
/**
* @event unseenChange
* @param {number} count Number of unseen items
*
* Items' seen status has changed
*/
/**
* @event unreadChange
* @param {number} count Number of unread items
*
* Items' read status has changed
*/
/* 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
* @fires unreadChange
*/
mw.echo.dm.NotificationsModel.prototype.onItemRead = function ( item, isRead ) {
var id = item && item.getId(),
unreadItem = id && this.unreadNotifications.getItemById( id );
if ( unreadItem ) {
if ( isRead ) {
this.unreadNotifications.removeItems( [ unreadItem ] );
} else {
this.unreadNotifications.addItems( [ unreadItem ] );
}
this.emit( 'unreadChange', this.unreadNotifications.getItems() );
}
};
/**
* Get the type of the notifications that this model deals with.
* Notifications type are given from the API: 'alert', 'message', 'all'
*
* @return {string} Notifications type
*/
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 () {
return this.unseenNotifications.getItemCount();
};
/**
* Get the counter of how many notifications are unread
*
* @return {number} Number of unread notifications
*/
mw.echo.dm.NotificationsModel.prototype.getUnreadCount = function () {
return this.unreadNotifications.getItemCount();
};
/**
* Set the system seen time - the last time we've marked notification as seen
*
* @private
* @param {string} Mediawiki seen timestamp in Mediawiki timestamp format
*/
mw.echo.dm.NotificationsModel.prototype.setSeenTime = function ( time ) {
this.seenTime[this.type] = time;
};
/**
* Get the system seen time
*
* @return {string} Mediawiki seen timestamp in Mediawiki timestamp format
*/
mw.echo.dm.NotificationsModel.prototype.getSeenTime = function () {
return this.seenTime[this.type];
};
/**
* Check whether the model is fetching notifications from the API
*
* @return {boolean} The model is in the process of fetching from the API
*/
mw.echo.dm.NotificationsModel.prototype.isFetchingNotifications = function () {
return !!this.fetchNotificationsPromise;
};
/**
* Return the fetch notifications promise
* @return {jQuery.Promise} Promise that is resolved when notifications were
* fetched from the API.
*/
mw.echo.dm.NotificationsModel.prototype.getFetchNotificationPromise = function () {
return this.fetchNotificationsPromise;
};
/**
* Update the seen timestamp
*
* @return {jQuery.Promise} A promise that resolves with the seen timestamp
* @fires updateSeenTime
*/
mw.echo.dm.NotificationsModel.prototype.updateSeenTime = function () {
var i, len,
model = this,
items = this.unseenNotifications.getItems();
// Update the notifications seen status
for ( i = 0, len = items.length; i < len; i++ ) {
items[i].toggleSeen( true );
}
this.emit( 'updateSeenTime' );
return this.api.postWithToken( 'edit', {
action: 'echomarkseen',
type: this.type
} )
.then( function ( data ) {
var time = data.query.echomarkseen.timestamp;
// Update seen time from the server
model.setSeenTime( time );
} );
};
/**
* Mark all notifications as read
*
* @return {jQuery.Promise} A promise that resolves when all notifications
* were marked as read.
*/
mw.echo.dm.NotificationsModel.prototype.markAllRead = function () {
var model = this,
data = {
action: 'echomarkread',
uselang: this.userLang,
sections: this.type
};
if ( !this.unreadNotifications.getItemCount() ) {
return $.Deferred().resolve( 0 ).promise();
}
return this.api.postWithToken( 'edit', data )
.then( function ( result ) {
return result.query.echomarkread[model.type].rawcount || 0;
} )
.then( function () {
var i, len,
items = model.unreadNotifications.getItems();
for ( i = 0, len = items.length; i < len; i++ ) {
items[i].toggleRead( true );
items[i].toggleSeen( true );
}
model.unreadNotifications.clearItems();
} );
};
/**
* Fetch notifications from the API and update the notifications list.
*
* @param {jQuery.Promise} An existing promise querying the API for notifications.
* This allows us to send an API request external to the DM and have the model
* handle the operation as if it asked for the request itself, updating all that
* needs to be updated and emitting all proper events.
* @return {jQuery.Promise} A promise that resolves with an array of notification
* id's.
*/
mw.echo.dm.NotificationsModel.prototype.fetchNotifications = function ( apiPromise ) {
var model = this,
params = $.extend( { notsections: this.type }, mw.echo.apiCallParams );
if ( !this.fetchNotificationsPromise ) {
this.fetchNotificationsPromise = ( apiPromise || this.api.get( params ) )
.then( function ( result ) {
var notifData, i, len, $content, wasSeen, wasRead, notificationModel,
optionItems = [],
idArray = [],
data = result.query.notifications[model.type];
for ( i = 0, len = data.index.length; i < len; i++ ) {
notifData = data.list[ data.index[i] ];
if ( model.getItemById( notifData.id ) ) {
// Skip if we already have the item
continue;
}
// TODO: This should really be formatted better, and the OptionWidget
// should be the one that displays whatever icon relates to this notification
// according to its type.
$content = $( $.parseHTML( notifData['*'] ) );
wasRead = !!notifData.read;
wasSeen = notifData.timestamp.mw <= model.getSeenTime();
notificationModel = new mw.echo.dm.NotificationItem(
notifData.id,
{
read: wasRead,
seen: wasSeen,
timestamp: notifData.timestamp.mw,
category: notifData.category,
content: $content,
// Hack: Get the primary link from the $content
primaryUrl: $content.find( '.mw-echo-notification-primary-link' ).attr( 'href' )
}
);
idArray.push( notifData.id );
optionItems.push( notificationModel );
}
model.addItems( optionItems, 0 );
return idArray;
} )
.then( function ( idArray ) {
model.fetchNotificationsPromise = null;
return idArray;
} );
}
return this.fetchNotificationsPromise;
};
/**
* Update the unread and unseen tracking lists when we add items
*
* @param {mw.echo.dm.NotificationItem[]} items Items to add
* @param {number} index Index to add items at
*/
mw.echo.dm.NotificationsModel.prototype.addItems = function ( items, index ) {
var i, len;
for ( i = 0, len = items.length; i < len; i++ ) {
if ( !items[i].isRead() ) {
this.unreadNotifications.addItems( [ items[i] ] );
}
if ( !items[i].isSeen() ) {
this.unseenNotifications.addItems( [ items[i] ] );
}
}
// Parent
mw.echo.dm.List.prototype.addItems.call( this, items, index );
};
/**
* Update the unread and unseen tracking lists when we remove items
*
* @param {mw.echo.dm.NotificationItem[]} items Items to remove
* @param {number} index Index to add items at
*/
mw.echo.dm.NotificationsModel.prototype.removeItems = function ( items ) {
var i, len;
for ( i = 0, len = items.length; i < len; i++ ) {
this.unreadNotifications.removeItems( [ items[i] ] );
this.unseenNotifications.removeItems( [ items[i] ] );
}
// Parent
mw.echo.dm.List.prototype.removeItems.call( this, items );
};
/**
* Update the unread and unseen tracking lists when we clear items
*/
mw.echo.dm.NotificationsModel.prototype.clearItems = function () {
this.unreadNotifications.clearItems();
this.unseenNotifications.clearItems();
// Parent
mw.echo.dm.List.prototype.clearItems.call( this );
};
/**
* 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
* and the badge label is updated.
*/
mw.echo.dm.NotificationsModel.prototype.fetchUnreadCountFromApi = function () {
var apiData = {
action: 'query',
meta: 'notifications',
notsections: this.getType(),
notmessageunreadfirst: 1,
notlimit: this.limit,
notprop: 'index|count',
uselang: this.userLang
};
return this.api.get( apiData )
.then( function ( result ) {
return OO.getProp( result.query, 'notifications', 'rawcount' ) || 0;
} );
};
} )( mediaWiki, jQuery );