Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
365 lines
11 KiB
JavaScript
Executable file
365 lines
11 KiB
JavaScript
Executable file
'use strict';
|
|
define(function(require) {
|
|
var evt = require('evt'),
|
|
// Expect a module to provide a function that allows setting up model/api
|
|
// pieces that depend on specific UI or localizations.
|
|
modelInit = require('model_init');
|
|
|
|
function dieOnFatalError(msg) {
|
|
console.error('FATAL:', msg);
|
|
throw new Error(msg);
|
|
}
|
|
|
|
function saveHasAccount(acctsSlice) {
|
|
// Save localStorage value to improve startup choices
|
|
localStorage.setItem('data_has_account',
|
|
(acctsSlice.items.length ? 'yes' : 'no'));
|
|
|
|
console.log('WRITING LOCAL STORAGE ITEM: ' + 'data_has_account',
|
|
(acctsSlice.items.length ? 'yes' : 'no'));
|
|
}
|
|
|
|
/**
|
|
* Provides a front end to the API and slice objects returned from the API.
|
|
* Since the UI right now is driven by a shared set of slices, this module
|
|
* tracks those slices and creates events when they are changed. This means
|
|
* the card modules do not need a direct reference to each other to change
|
|
* the backing data for a card, and that card modules and app logic do not
|
|
* need a hard, static dependency on the MailAPI object. This allows some
|
|
* more flexible and decoupled loading scenarios. In particular, cards can
|
|
* be created an inserted into the DOM without needing the back end to
|
|
* complete its startup and initialization.
|
|
*
|
|
* It mixes in 'evt' capabilities, so it will be common to see model
|
|
* used with 'latest' and 'latestOnce' to get the latest model data
|
|
* whenever it loads.
|
|
*
|
|
* Down the road, it may make sense to have more than one model object
|
|
* in play. At that point, it may make sense to morph this into a
|
|
* constructor function and then have the card objects receive a model
|
|
* instance property for their model reference.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
function Model() {
|
|
evt.Emitter.call(this);
|
|
}
|
|
|
|
Model.prototype = {
|
|
/**
|
|
* acctsSlice event is fired when the property changes.
|
|
* event: acctsSlice
|
|
* @param {Object} the acctsSlice object.
|
|
**/
|
|
acctsSlice: null,
|
|
|
|
/**
|
|
* account event is fired when the property changes.
|
|
* event: account
|
|
* @param {Object} the account object.
|
|
**/
|
|
account: null,
|
|
|
|
/**
|
|
* foldersSlice event is fired when the property changes.
|
|
* event: foldersSlice
|
|
* @param {Object} the foldersSlice object.
|
|
**/
|
|
foldersSlice: null,
|
|
|
|
/**
|
|
* folder event is fired when the property changes.
|
|
* event: folder
|
|
* @param {Object} the folder object.
|
|
**/
|
|
folder: null,
|
|
|
|
/**
|
|
* emits an event based on a property value. Since the
|
|
* event is based on a property value that is on this
|
|
* object, *do not* use emitWhenListener, since, due to
|
|
* the possibility of queuing old values with that
|
|
* method, it could cause bad results (bug 971617), and
|
|
* it is not needed since the latest* methods will get
|
|
* the latest value on this object.
|
|
* @param {String} id event ID/property name
|
|
*/
|
|
_callEmit: function(id) {
|
|
this.emit(id, this[id]);
|
|
},
|
|
|
|
inited: false,
|
|
|
|
/**
|
|
* Returns true if there is an account. Should only be
|
|
* called after inited is true.
|
|
*/
|
|
hasAccount: function() {
|
|
return (this.getAccountCount() > 0);
|
|
},
|
|
|
|
/**
|
|
* Given an account ID, get the account object. Only works once the
|
|
* acctsSlice property is available. Use model.latestOnce to get a
|
|
* handle on an acctsSlice property, then call this method.
|
|
* @param {String} id account ID.
|
|
* @return {Object} account object.
|
|
*/
|
|
getAccount: function(id) {
|
|
if (!this.acctsSlice || !this.acctsSlice.items) {
|
|
throw new Error('No acctsSlice available');
|
|
}
|
|
|
|
var targetAccount;
|
|
this.acctsSlice.items.some(function(account) {
|
|
if (account.id === id) {
|
|
return !!(targetAccount = account);
|
|
}
|
|
});
|
|
|
|
return targetAccount;
|
|
},
|
|
|
|
/**
|
|
* Get the numbers of configured account.
|
|
* Should only be called after this.inited is true.
|
|
* @return {Number} numbers of account.
|
|
*/
|
|
getAccountCount: function() {
|
|
var count = 0;
|
|
|
|
if (this.acctsSlice &&
|
|
this.acctsSlice.items &&
|
|
this.acctsSlice.items.length) {
|
|
count = this.acctsSlice.items.length;
|
|
}
|
|
|
|
return count;
|
|
},
|
|
|
|
/**
|
|
* Call this to initialize the model. It can be called more than once
|
|
* per the lifetime of an app. The usual use case for multiple calls
|
|
* is when a new account has been added.
|
|
*
|
|
* It is *not* called by default in this module to allow for lazy startup,
|
|
* and for cases like unit tests that may not want to trigger a full model
|
|
* creation for a simple UI test.
|
|
*
|
|
* @param {boolean} showLatest Choose the latest account in the
|
|
* acctsSlice. Otherwise it choose the account marked as the default
|
|
* account.
|
|
*/
|
|
init: function(showLatest, callback) {
|
|
require(['api'], function(api) {
|
|
// Multiple model instances can be created, but only one init needs
|
|
// to be done with the backend API.
|
|
if (this === modelCreate.defaultModel) {
|
|
modelInit(this, api);
|
|
}
|
|
|
|
this.api = api;
|
|
|
|
// If already initialized before, clear out previous state.
|
|
this.die();
|
|
|
|
var acctsSlice = api.viewAccounts(false);
|
|
acctsSlice.oncomplete = (function() {
|
|
// To prevent a race between Model.init() and
|
|
// acctsSlice.oncomplete, only assign model.acctsSlice when
|
|
// the slice has actually loaded (i.e. after
|
|
// acctsSlice.oncomplete fires).
|
|
this.acctsSlice = acctsSlice;
|
|
|
|
saveHasAccount(acctsSlice);
|
|
|
|
if (acctsSlice.items.length) {
|
|
// For now, just use the first one; we do attempt to put unified
|
|
// first so this should generally do the right thing.
|
|
// XXX: Because we don't have unified account now, we should
|
|
// switch to the latest account which user just added.
|
|
var account = showLatest ? acctsSlice.items.slice(-1)[0] :
|
|
acctsSlice.defaultAccount;
|
|
|
|
this.changeAccount(account, callback);
|
|
} else if (callback) {
|
|
callback();
|
|
}
|
|
|
|
this.inited = true;
|
|
this._callEmit('acctsSlice');
|
|
|
|
// Once the API/worker has started up and we have received account
|
|
// data, consider the app fully loaded: we have verified full flow
|
|
// of data from front to back.
|
|
if (this === modelCreate.defaultModel) {
|
|
evt.emitWhenListener('metrics:apiDone');
|
|
}
|
|
}).bind(this);
|
|
|
|
acctsSlice.onchange = function() {
|
|
saveHasAccount(acctsSlice);
|
|
}.bind(this);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Changes the current account tracked by the model. This results
|
|
* in changes to the 'account', 'foldersSlice' and 'folder' properties.
|
|
* @param {Object} account the account object.
|
|
* @param {Function} callback function to call once the account and
|
|
* related folder data has changed.
|
|
*/
|
|
changeAccount: function(account, callback) {
|
|
// Do not bother if account is the same.
|
|
if (this.account && this.account.id === account.id) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this._dieFolders();
|
|
|
|
this.account = account;
|
|
this._callEmit('account');
|
|
|
|
var foldersSlice = this.api.viewFolders('account', account);
|
|
foldersSlice.oncomplete = (function() {
|
|
this.foldersSlice = foldersSlice;
|
|
this.foldersSlice.onchange = this.notifyFoldersSliceOnChange.bind(this);
|
|
this.selectInbox(callback);
|
|
this._callEmit('foldersSlice');
|
|
}).bind(this);
|
|
},
|
|
|
|
/**
|
|
* Given an account ID, change the current account to that account.
|
|
* @param {String} accountId
|
|
* @return {Function} callback
|
|
*/
|
|
changeAccountFromId: function(accountId, callback) {
|
|
if (!this.acctsSlice || !this.acctsSlice.items.length) {
|
|
throw new Error('No accounts available');
|
|
}
|
|
|
|
this.acctsSlice.items.some(function(account) {
|
|
if (account.id === accountId) {
|
|
this.changeAccount(account, callback);
|
|
return true;
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Just changes the folder property tracked by the model.
|
|
* Assumes the folder still belongs to the currently tracked
|
|
* account. It also does not result in any state changes or
|
|
* event emitting if the new folder is the same as the
|
|
* currently tracked folder.
|
|
* @param {Object} folder the folder object to use.
|
|
*/
|
|
changeFolder: function(folder) {
|
|
if (folder && (!this.folder || folder.id !== this.folder.id)) {
|
|
this.folder = folder;
|
|
this._callEmit('folder');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* For the already loaded account and associated foldersSlice,
|
|
* set the inbox as the tracked 'folder'.
|
|
* @param {Function} callback function called once the inbox
|
|
* has been selected.
|
|
*/
|
|
selectInbox: function(callback) {
|
|
this.selectFirstFolderWithType('inbox', callback);
|
|
},
|
|
|
|
/**
|
|
* For the already loaded account and associated foldersSlice, set
|
|
* the given folder as the tracked folder. The account MUST have a
|
|
* folder with the given type, or a fatal error will occur.
|
|
*/
|
|
selectFirstFolderWithType: function(folderType, callback) {
|
|
if (!this.foldersSlice) {
|
|
throw new Error('No foldersSlice available');
|
|
}
|
|
|
|
var folder = this.foldersSlice.getFirstFolderWithType(folderType);
|
|
if (!folder) {
|
|
dieOnFatalError('We have an account without a folderType ' +
|
|
folderType + '!', this.foldersSlice.items);
|
|
}
|
|
|
|
if (this.folder && this.folder.id === folder.id) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
} else {
|
|
if (callback) {
|
|
this.once('folder', callback);
|
|
}
|
|
this.changeFolder(folder);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called by other code when it knows the current account
|
|
* has received new inbox messages. Just triggers an
|
|
* event with the count for now.
|
|
* @param {Object} accountUpdate update object from
|
|
* sync.js accountResults object structure.
|
|
*/
|
|
notifyInboxMessages: function(accountUpdate) {
|
|
if (accountUpdate.id === this.account.id) {
|
|
this.emit('newInboxMessages', accountUpdate.count);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Triggered by the foldersSlice onchange event
|
|
* @param {Object} folder the folder that changed.
|
|
*/
|
|
notifyFoldersSliceOnChange: function(folder) {
|
|
this.emit('foldersSliceOnChange', folder);
|
|
},
|
|
|
|
notifyBackgroundSendStatus: function(data) {
|
|
this.emit('backgroundSendStatus', data);
|
|
},
|
|
|
|
// Lifecycle
|
|
|
|
_dieFolders: function() {
|
|
if (this.foldersSlice) {
|
|
this.foldersSlice.die();
|
|
}
|
|
this.foldersSlice = null;
|
|
|
|
this.folder = null;
|
|
},
|
|
|
|
die: function() {
|
|
if (this.acctsSlice) {
|
|
this.acctsSlice.die();
|
|
}
|
|
this.acctsSlice = null;
|
|
this.account = null;
|
|
|
|
this._dieFolders();
|
|
}
|
|
};
|
|
|
|
evt.mix(Model.prototype);
|
|
|
|
function modelCreate() {
|
|
return new Model();
|
|
}
|
|
|
|
// Create a default one that can be used by setup code that does not need a
|
|
// specific model instance, just one that should be used by default.
|
|
modelCreate.defaultModel = new Model();
|
|
|
|
return modelCreate;
|
|
});
|