mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-09 01:35:52 +08:00
Fuck it - tasks don’t performLocal either
This commit is contained in:
parent
6d7fe519d9
commit
0f6fdb4256
15 changed files with 78 additions and 676 deletions
|
@ -98,6 +98,7 @@ export default class CategoryPickerPopover extends Component {
|
|||
}
|
||||
const item = category.toJSON()
|
||||
item.category = category
|
||||
item.displayName = category.displayName
|
||||
item.backgroundColor = LabelColorizer.backgroundColorDark(category)
|
||||
item.usage = usageCount[category.id] || 0
|
||||
item.numThreads = numThreads
|
||||
|
@ -316,7 +317,7 @@ export default class CategoryPickerPopover extends Component {
|
|||
<div className="category-item">
|
||||
{icon}
|
||||
<div className="category-display-name">
|
||||
<BoldedSearchResult value={item.display_name} query={this.state.searchValue || ""} />
|
||||
<BoldedSearchResult value={item.displayName} query={this.state.searchValue || ""} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -21,9 +21,9 @@ describe 'Category', ->
|
|||
|
||||
describe 'fromJSON', ->
|
||||
it "should strip the INBOX. prefix from FastMail folders", ->
|
||||
foo = (new Category()).fromJSON({display_name: 'INBOX.Foo'})
|
||||
foo = (new Category()).fromJSON({displayName: 'INBOX.Foo'})
|
||||
expect(foo.displayName).toEqual('Foo')
|
||||
foo = (new Category()).fromJSON({display_name: 'INBOX'})
|
||||
foo = (new Category()).fromJSON({displayName: 'INBOX'})
|
||||
expect(foo.displayName).toEqual('Inbox')
|
||||
|
||||
describe 'category types', ->
|
||||
|
|
|
@ -9,7 +9,7 @@ describe 'Thread', ->
|
|||
describe 'serialization performance', ->
|
||||
xit '1,000,000 iterations', ->
|
||||
iterations = 0
|
||||
json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
|
||||
json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","displayName":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
|
||||
start = Date.now()
|
||||
while iterations < 1000000
|
||||
if _.isString(json)
|
||||
|
|
|
@ -20,7 +20,7 @@ xdescribe "DestroyCategoryTask", ->
|
|||
fn.calls[0].args[0].accountId
|
||||
|
||||
nameOf = (fn) ->
|
||||
fn.calls[0].args[0].body.display_name
|
||||
fn.calls[0].args[0].body.displayName
|
||||
|
||||
makeAccount = ({usesFolders, usesLabels} = {}) ->
|
||||
spyOn(AccountStore, "accountForId").andReturn {
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import _ from 'underscore';
|
||||
import net from 'net';
|
||||
import fs from 'fs';
|
||||
import Actions from './actions';
|
||||
import DatabaseStore from './stores/database-store';
|
||||
import DatabaseChangeRecord from './stores/database-change-record';
|
||||
|
||||
import Utils from './models/utils';
|
||||
|
||||
const Message = {
|
||||
DATABASE_STORE_TRIGGER: 'db-store-trigger',
|
||||
};
|
||||
|
||||
const printToConsole = false;
|
||||
|
||||
class ActionBridgeCPP {
|
||||
|
||||
constructor() {
|
||||
|
@ -27,10 +19,12 @@ class ActionBridgeCPP {
|
|||
console.info(err);
|
||||
}
|
||||
|
||||
this.clients = [];
|
||||
|
||||
// This server listens on a Unix socket at /var/run/mysocket
|
||||
const unixServer = net.createServer((c) => {
|
||||
// Do something with the client connection
|
||||
console.log('client connected');
|
||||
this.clients.push(c);
|
||||
c.on('data', (d) => {
|
||||
this.onIncomingMessage(d.toString());
|
||||
});
|
||||
|
@ -43,9 +37,8 @@ class ActionBridgeCPP {
|
|||
|
||||
c.on('end', () => {
|
||||
console.log('client disconnected');
|
||||
this.clients = this.clients.filter((o) => o !== c);
|
||||
});
|
||||
// c.write('hello\r\n');
|
||||
// c.pipe(c);
|
||||
});
|
||||
|
||||
unixServer.listen('/tmp/cmail.sock', () => {
|
||||
|
@ -77,6 +70,19 @@ class ActionBridgeCPP {
|
|||
}
|
||||
}
|
||||
|
||||
onTellClients(json) {
|
||||
const msg = JSON.stringify(json, Utils.registeredObjectReplacer);
|
||||
const headerBuffer = new Buffer(4);
|
||||
const contentBuffer = Buffer.from(msg);
|
||||
headerBuffer.fill(0);
|
||||
headerBuffer.writeUInt32LE(contentBuffer.length, 0);
|
||||
|
||||
for (const c of this.clients) {
|
||||
c.write(headerBuffer);
|
||||
c.write(contentBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnload(readyToUnload) {
|
||||
// Unfortunately, if you call ipc.send and then immediately close the window,
|
||||
// Electron won't actually send the message. To work around this, we wait an
|
||||
|
|
|
@ -87,14 +87,13 @@ export default class Category extends Model {
|
|||
queryable: true,
|
||||
modelKey: 'path',
|
||||
}),
|
||||
imapName: Attributes.String({
|
||||
modelKey: 'imapName',
|
||||
jsonKey: 'imap_name',
|
||||
}),
|
||||
syncProgress: Attributes.Object({
|
||||
modelKey: 'syncProgress',
|
||||
jsonKey: 'sync_progress',
|
||||
}),
|
||||
_refcount: Attributes.Number({
|
||||
modelKey: '_refcount',
|
||||
}),
|
||||
});
|
||||
|
||||
static Types = {
|
||||
|
|
|
@ -147,6 +147,10 @@ export default class Message extends ModelWithMetadata {
|
|||
modelKey: 'subject',
|
||||
}),
|
||||
|
||||
folderImapUID: Attributes.Number({
|
||||
modelKey: 'folderImapUID',
|
||||
}),
|
||||
|
||||
draft: Attributes.Boolean({
|
||||
modelKey: 'draft',
|
||||
queryable: true,
|
||||
|
|
|
@ -30,16 +30,14 @@ class RecentlyReadStore extends NylasStore {
|
|||
|
||||
tasks.filter(task =>
|
||||
task instanceof ChangeUnreadTask
|
||||
).forEach(({threads}) => {
|
||||
const threadIds = threads.map(t => (t.id ? t.id : t));
|
||||
).forEach(({threadIds}) => {
|
||||
this.ids = this.ids.concat(threadIds);
|
||||
changed = true;
|
||||
});
|
||||
|
||||
tasks.filter(task =>
|
||||
task instanceof ChangeLabelsTask || task instanceof ChangeFolderTask
|
||||
).forEach(({threads}) => {
|
||||
const threadIds = threads.map(t => (t.id ? t.id : t));
|
||||
).forEach(({threadIds}) => {
|
||||
this.ids = this.ids.filter(id => !threadIds.includes(id));
|
||||
changed = true;
|
||||
});
|
||||
|
|
|
@ -124,20 +124,14 @@ class TaskQueue extends NylasStore {
|
|||
console.log(task);
|
||||
throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`");
|
||||
}
|
||||
if (!task.queueState) {
|
||||
console.log(task);
|
||||
throw new Error("Tasks must have a queueState prior to being queued. Check that your Task constructor is calling `super`");
|
||||
}
|
||||
task.sequentialId = ++this._currentSequentialId;
|
||||
task.runLocal().then(() => {
|
||||
DatabaseStore.inTransaction((t) => {
|
||||
return t.persistModel(task);
|
||||
});
|
||||
});
|
||||
task.status = 'local';
|
||||
|
||||
NylasEnv.actionBridgeCpp.onTellClients({type: 'task-queued', task: task});
|
||||
}
|
||||
|
||||
enqueueUndoOfTaskId = (taskId) => {
|
||||
const task = this._queue.find(t => t.id == taskId) || this._completed.find(t => t.id == taskId);
|
||||
const task = this._queue.find(t => t.id === taskId) || this._completed.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
this.enqueue(task.createUndoTask());
|
||||
}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import _ from 'underscore';
|
||||
import Thread from '../models/thread';
|
||||
import Category from '../models/category';
|
||||
import Message from '../models/message';
|
||||
import Actions from '../actions'
|
||||
import DatabaseStore from '../stores/database-store';
|
||||
import ChangeMailTask from './change-mail-task';
|
||||
import SyncbackCategoryTask from './syncback-category-task';
|
||||
|
||||
// Public: Create a new task to apply labels to a message or thread.
|
||||
//
|
||||
|
@ -34,10 +28,6 @@ export default class ChangeFolderTask extends ChangeMailTask {
|
|||
return "Moving to folder";
|
||||
}
|
||||
|
||||
categoriesToAdd() {
|
||||
return [this.folder];
|
||||
}
|
||||
|
||||
description() {
|
||||
if (this.taskDescription) {
|
||||
return this.taskDescription;
|
||||
|
@ -48,26 +38,22 @@ export default class ChangeFolderTask extends ChangeMailTask {
|
|||
folderText = ` to ${this.folder.displayName}`;
|
||||
}
|
||||
|
||||
if (this.threads.length > 1) {
|
||||
return `Moved ${this.threads.length} threads${folderText}`;
|
||||
} else if (this.messages.length > 1) {
|
||||
return `Moved ${this.messages.length} messages${folderText}`;
|
||||
if (this.threadIds.length > 1) {
|
||||
return `Moved ${this.threadIds.length} threads${folderText}`;
|
||||
} else if (this.messageIds.length > 1) {
|
||||
return `Moved ${this.messageIds.length} messages${folderText}`;
|
||||
}
|
||||
return `Moved${folderText}`;
|
||||
}
|
||||
|
||||
isDependentOnTask(other) {
|
||||
return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
if (!this.folder) {
|
||||
return Promise.reject(new Error("Must specify a `folder`"))
|
||||
}
|
||||
if (this.threads.length > 0 && this.messages.length > 0) {
|
||||
if (this.threadIds.length > 0 && this.messageIds.length > 0) {
|
||||
return Promise.reject(new Error("ChangeFolderTask: You can move `threads` or `messages` but not both"))
|
||||
}
|
||||
if (this.threads.length === 0 && this.messages.length === 0) {
|
||||
if (this.threadIds.length === 0 && this.messageIds.length === 0) {
|
||||
return Promise.reject(new Error("ChangeFolderTask: You must provide a `threads` or `messages` Array of models or IDs."))
|
||||
}
|
||||
|
||||
|
@ -77,64 +63,4 @@ export default class ChangeFolderTask extends ChangeMailTask {
|
|||
_isArchive() {
|
||||
return this.folder.name === "archive" || this.folder.name === "all"
|
||||
}
|
||||
|
||||
recordUserEvent() {
|
||||
if (this.source === "Mail Rules") {
|
||||
return
|
||||
}
|
||||
Actions.recordUserEvent("Threads Moved to Folder", {
|
||||
source: this.source,
|
||||
isArchive: this._isArchive(),
|
||||
folderType: this.folder.name || "custom",
|
||||
folderDisplayName: this.folder.displayName,
|
||||
numThreads: this.threads.length,
|
||||
numMessages: this.messages.length,
|
||||
description: this.description(),
|
||||
isUndo: this._isUndoTask,
|
||||
})
|
||||
}
|
||||
|
||||
retrieveModels() {
|
||||
return Promise.props({
|
||||
folder: DatabaseStore.modelify(Category, [this.folder]),
|
||||
threads: DatabaseStore.modelify(Thread, this.threads),
|
||||
messages: DatabaseStore.modelify(Message, this.messages),
|
||||
|
||||
}).then(({folder, threads, messages}) => {
|
||||
// Remove any objects we weren't able to find. This can happen pretty easily
|
||||
// if (you undo an action && other things have happened.)
|
||||
this.folder = folder[0];
|
||||
this.threads = _.compact(threads);
|
||||
this.messages = _.compact(messages);
|
||||
|
||||
if (!this.folder) {
|
||||
return Promise.reject(new Error("The specified folder could not be found."));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
processNestedMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
changesToModel(model) {
|
||||
if (model instanceof Thread) {
|
||||
return {categories: [this.folder]}
|
||||
}
|
||||
if (model instanceof Message) {
|
||||
return {categories: [this.folder]}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
requestBodyForModel(model) {
|
||||
if (model instanceof Thread) {
|
||||
return {folder: model.folders[0] ? model.folders[0].id : null};
|
||||
}
|
||||
if (model instanceof Message) {
|
||||
return {folder: model.folder ? model.folder.id : null};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import _ from 'underscore';
|
||||
import Thread from '../models/thread';
|
||||
import Message from '../models/message';
|
||||
import Actions from '../actions'
|
||||
import Category from '../models/category';
|
||||
import DatabaseStore from '../stores/database-store';
|
||||
import CategoryStore from '../stores/category-store';
|
||||
import AccountStore from '../stores/account-store';
|
||||
import ChangeMailTask from './change-mail-task';
|
||||
import SyncbackCategoryTask from './syncback-category-task';
|
||||
|
||||
// Public: Create a new task to apply labels to a message or thread.
|
||||
//
|
||||
|
@ -44,8 +36,8 @@ export default class ChangeLabelsTask extends ChangeMailTask {
|
|||
}
|
||||
|
||||
let countString = "";
|
||||
if (this.threads.length > 1) {
|
||||
countString = ` ${this.threads.length} threads`;
|
||||
if (this.threadIds.length > 1) {
|
||||
countString = ` ${this.threadIds.length} threads`;
|
||||
}
|
||||
|
||||
const removed = this.labelsToRemove[0];
|
||||
|
@ -88,163 +80,8 @@ export default class ChangeLabelsTask extends ChangeMailTask {
|
|||
return `Changed labels${countString ? ' on' : ''}${countString}`;
|
||||
}
|
||||
|
||||
isDependentOnTask(other) {
|
||||
return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);
|
||||
}
|
||||
|
||||
// In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and
|
||||
// they are mutually exclusive, so we need to make sure that any add/remove
|
||||
// label operation still guarantees that constraint
|
||||
_ensureAndUpdateLabels(account, existingLabelsToAdd, existingLabelsToRemove = {}) {
|
||||
const labelsToAdd = existingLabelsToAdd;
|
||||
let labelsToRemove = existingLabelsToRemove;
|
||||
|
||||
const setToAdd = new Set(_.compact(_.pluck(labelsToAdd, 'name')));
|
||||
const setToRemove = new Set(_.compact(_.pluck(labelsToRemove, 'name')));
|
||||
|
||||
if (setToRemove.has('all')) {
|
||||
if (!setToAdd.has('spam') && !setToAdd.has('trash')) {
|
||||
labelsToRemove = _.reject(labelsToRemove, label => label.name === 'all');
|
||||
}
|
||||
} else if (setToAdd.has('all')) {
|
||||
if (!setToRemove.has('trash')) {
|
||||
labelsToRemove.push(CategoryStore.getTrashCategory(account));
|
||||
}
|
||||
if (!setToRemove.has('spam')) {
|
||||
labelsToRemove.push(CategoryStore.getSpamCategory(account));
|
||||
}
|
||||
}
|
||||
|
||||
if (setToRemove.has('trash')) {
|
||||
if (!setToAdd.has('spam') && !setToAdd.has('all')) {
|
||||
labelsToAdd.push(CategoryStore.getAllMailCategory(account));
|
||||
}
|
||||
} else if (setToAdd.has('trash')) {
|
||||
if (!setToRemove.has('all')) {
|
||||
labelsToRemove.push(CategoryStore.getAllMailCategory(account))
|
||||
}
|
||||
if (!setToRemove.has('spam')) {
|
||||
labelsToRemove.push(CategoryStore.getSpamCategory(account))
|
||||
}
|
||||
}
|
||||
|
||||
if (setToRemove.has('spam')) {
|
||||
if (!setToAdd.has('trash') && !setToAdd.has('all')) {
|
||||
labelsToAdd.push(CategoryStore.getAllMailCategory(account));
|
||||
}
|
||||
} else if (setToAdd.has('spam')) {
|
||||
if (!setToRemove.has('all')) {
|
||||
labelsToRemove.push(CategoryStore.getAllMailCategory(account))
|
||||
}
|
||||
if (!setToRemove.has('trash')) {
|
||||
labelsToRemove.push(CategoryStore.getTrashCategory(account))
|
||||
}
|
||||
}
|
||||
|
||||
// This should technically not be possible, but we like to keep it safe
|
||||
return {
|
||||
labelsToAdd: _.compact(labelsToAdd),
|
||||
labelsToRemove: _.compact(labelsToRemove),
|
||||
};
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
if (this.messages.length > 0) {
|
||||
return Promise.reject(new Error("ChangeLabelsTask: N1 does not support viewing or changing labels on individual messages."))
|
||||
}
|
||||
if (this.labelsToAdd.length === 0 && this.labelsToRemove.length === 0) {
|
||||
return Promise.reject(new Error("ChangeLabelsTask: Must specify `labelsToAdd` or `labelsToRemove`"))
|
||||
}
|
||||
if (this.threads.length > 0 && this.messages.length > 0) {
|
||||
return Promise.reject(new Error("ChangeLabelsTask: You can move `threads` or `messages` but not both"))
|
||||
}
|
||||
if (this.threads.length === 0 && this.messages.length === 0) {
|
||||
return Promise.reject(new Error("ChangeLabelsTask: You must provide a `threads` or `messages` Array of models or IDs."))
|
||||
}
|
||||
|
||||
return super.performLocal();
|
||||
}
|
||||
|
||||
_isArchive() {
|
||||
const toAdd = this.labelsToAdd.map(l => l.name)
|
||||
return toAdd.includes("all") || toAdd.includes("archive")
|
||||
}
|
||||
|
||||
recordUserEvent() {
|
||||
if (this.source === "Mail Rules") {
|
||||
return
|
||||
}
|
||||
Actions.recordUserEvent("Threads Changed Labels", {
|
||||
source: this.source,
|
||||
isArchive: this._isArchive(),
|
||||
labelTypesToAdd: this.labelsToAdd.map(l => l.name || "custom"),
|
||||
labelTypesToRemove: this.labelsToRemove.map(l => l.name || "custom"),
|
||||
labelDisplayNamesToAdd: this.labelsToAdd.map(l => l.displayName),
|
||||
labelDisplayNamesToRemove: this.labelsToRemove.map(l => l.displayName),
|
||||
numThreads: this.threads.length,
|
||||
numMessages: this.messages.length,
|
||||
description: this.description(),
|
||||
isUndo: this._isUndoTask,
|
||||
})
|
||||
}
|
||||
|
||||
retrieveModels() {
|
||||
// Convert arrays of IDs or models to models.
|
||||
// modelify returns immediately if (no work is required)
|
||||
return Promise.props({
|
||||
labelsToAdd: DatabaseStore.modelify(Category, this.labelsToAdd),
|
||||
labelsToRemove: DatabaseStore.modelify(Category, this.labelsToRemove),
|
||||
threads: DatabaseStore.modelify(Thread, this.threads),
|
||||
messages: DatabaseStore.modelify(Message, this.messages),
|
||||
|
||||
}).then(({labelsToAdd, labelsToRemove, threads, messages}) => {
|
||||
if (_.any([].concat(labelsToAdd, labelsToRemove), _.isUndefined)) {
|
||||
return Promise.reject(new Error("One or more of the specified labels could not be found."))
|
||||
}
|
||||
const account = AccountStore.accountForItems(threads);
|
||||
if (!account) {
|
||||
return Promise.reject(new Error("ChangeLabelsTask: You must provide a set of `threads` from the same Account"))
|
||||
}
|
||||
// In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and
|
||||
// they are mutually exclusive, so we need to make sure that any add/remove
|
||||
// label operation still guarantees that constraint
|
||||
const updated = this._ensureAndUpdateLabels(account, labelsToAdd, labelsToRemove)
|
||||
|
||||
// Remove any objects we weren't able to find. This can happen pretty easily
|
||||
// if (you undo an action && other things have happened.)
|
||||
this.labelsToAdd = updated.labelsToAdd;
|
||||
this.labelsToRemove = updated.labelsToRemove;
|
||||
this.threads = _.compact(threads);
|
||||
this.messages = _.compact(messages);
|
||||
|
||||
// The base class does the heavy lifting and calls changesToModel
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
processNestedMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
changesToModel(model) {
|
||||
const labelsToRemoveIds = _.pluck(this.labelsToRemove, 'id')
|
||||
|
||||
let labels = _.reject(model.labels, ({id}) => labelsToRemoveIds.includes(id));
|
||||
labels = labels.concat(this.labelsToAdd);
|
||||
labels = _.uniq(labels, false, label => label.id);
|
||||
return {labels};
|
||||
}
|
||||
|
||||
requestBodyForModel(model) {
|
||||
const folder = model.labels.find(l => l.object === 'folder')
|
||||
const labels = model.labels.filter(l => l.object === 'label')
|
||||
|
||||
if (folder) {
|
||||
return {
|
||||
folder: folder.id,
|
||||
labels: labels.map(l => l.id),
|
||||
}
|
||||
}
|
||||
return {labels};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,6 @@ import _ from 'underscore';
|
|||
import Task from './task';
|
||||
import Thread from '../models/thread';
|
||||
import Message from '../models/message';
|
||||
import NylasAPI from '../nylas-api';
|
||||
import DatabaseStore from '../stores/database-store';
|
||||
import EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task'
|
||||
import BaseDraftTask from './base-draft-task'
|
||||
|
||||
/*
|
||||
Public: The ChangeMailTask is a base class for all tasks that modify sets
|
||||
|
@ -30,164 +26,18 @@ export default class ChangeMailTask extends Task {
|
|||
constructor({threads, thread, messages, message} = {}) {
|
||||
super();
|
||||
|
||||
this.threads = threads || [];
|
||||
const t = threads || [];
|
||||
if (thread) {
|
||||
this.threads.push(thread);
|
||||
t.push(thread);
|
||||
}
|
||||
this.messages = messages || [];
|
||||
const m = messages || [];
|
||||
if (message) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for subclasses
|
||||
|
||||
// Public: Override this method and return an object with key-value pairs
|
||||
// representing changed values. For example, if (your task sets unread:)
|
||||
// false, return {unread: false}.
|
||||
//
|
||||
// - `model` an individual {Thread} or {Message}
|
||||
//
|
||||
// Returns an object whos key-value pairs represent the desired changed
|
||||
// object.
|
||||
changesToModel() {
|
||||
throw new Error("You must override this method.");
|
||||
}
|
||||
|
||||
// Public: Override this method and return an object that will be the
|
||||
// request body used for saving changes to `model`.
|
||||
//
|
||||
// - `model` an individual {Thread} or {Message}
|
||||
//
|
||||
// Returns an object that will be passed as the `body` to the actual API
|
||||
// `request` object
|
||||
requestBodyForModel() {
|
||||
throw new Error("You must override this method.");
|
||||
}
|
||||
|
||||
// Public: Override to indicate whether actions need to be taken for all
|
||||
// messages of each thread.
|
||||
//
|
||||
// Generally, you cannot provide both messages and threads at the same
|
||||
// time. However, ChangeMailTask runs for provided threads first and then
|
||||
// messages. Override and return true, and you will receive
|
||||
// `changesToModel` for messages in changed threads, and any changes you
|
||||
// make will be written to the database and undone during undo.
|
||||
//
|
||||
// Note that API requests are only made for threads if (threads are)
|
||||
// present.
|
||||
processNestedMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Public: Returns categories that this task will add to the set of threads
|
||||
// Must be overriden
|
||||
categoriesToAdd() {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Public: Returns categories that this task will remove the set of threads
|
||||
// Must be overriden
|
||||
categoriesToRemove() {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Public: Subclasses should override `performLocal` and call super once
|
||||
// they've prepared the data they need and verified that requirements are
|
||||
// met.
|
||||
|
||||
// See {Task::performLocal} for more usage info
|
||||
|
||||
performLocal() {
|
||||
if (this._isUndoTask && !this._restoreValues) {
|
||||
return Promise.reject(new Error("ChangeMailTask: No _restoreValues provided for undo task."))
|
||||
}
|
||||
// Lock the models with the optimistic change tracker so they aren't reverted
|
||||
// while the user is seeing our optimistic changes.
|
||||
if (!this._isReverting) {
|
||||
this._lockAll();
|
||||
m.push(message);
|
||||
}
|
||||
|
||||
return DatabaseStore.inTransaction((t) => {
|
||||
return this.retrieveModels().then(() => {
|
||||
return this._performLocalThreads(t)
|
||||
}).then(() => {
|
||||
return this._performLocalMessages(t)
|
||||
})
|
||||
}).then(() => {
|
||||
try {
|
||||
this.recordUserEvent()
|
||||
} catch (err) {
|
||||
NylasEnv.reportError(err);
|
||||
// don't throw
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recordUserEvent() {
|
||||
throw new Error("Override recordUserEvent")
|
||||
}
|
||||
|
||||
retrieveModels() {
|
||||
// Note: Currently, *ALL* subclasses must use `DatabaseStore.modelify`
|
||||
// to convert `threads` and `messages` from models or ids to models.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
_performLocalThreads(transaction) {
|
||||
const changed = this._applyChanges(this.threads);
|
||||
const changedIds = _.pluck(changed, 'id');
|
||||
|
||||
if (changed.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return transaction.persistModels(changed).then(() => {
|
||||
if (!this.processNestedMessages()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return DatabaseStore.findAll(Message).where(Message.attributes.threadId.in(changedIds)).then((messages) => {
|
||||
this.messages = [].concat(messages, this.messages);
|
||||
return Promise.resolve()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
_performLocalMessages(transaction) {
|
||||
const changed = this._applyChanges(this.messages);
|
||||
return (changed.length > 0) ? transaction.persistModels(changed) : Promise.resolve();
|
||||
}
|
||||
|
||||
_applyChanges(modelArray) {
|
||||
const changed = [];
|
||||
|
||||
if (this._shouldChangeBackwards()) {
|
||||
modelArray.forEach((model, idx) => {
|
||||
if (this._restoreValues[model.id]) {
|
||||
const updated = _.extend(model.clone(), this._restoreValues[model.id]);
|
||||
modelArray[idx] = updated;
|
||||
changed.push(updated);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._restoreValues = this._restoreValues || {};
|
||||
modelArray.forEach((model, idx) => {
|
||||
const fieldsNew = this.changesToModel(model);
|
||||
const fieldsCurrent = _.pick(model, Object.keys(fieldsNew));
|
||||
if (!_.isEqual(fieldsCurrent, fieldsNew)) {
|
||||
this._restoreValues[model.id] = fieldsCurrent;
|
||||
const updated = _.extend(model.clone(), fieldsNew);
|
||||
modelArray[idx] = updated;
|
||||
changed.push(updated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
_shouldChangeBackwards() {
|
||||
return this._isReverting || this._isUndoTask;
|
||||
// we actually only keep a small bit of data now
|
||||
this.threadIds = t.map(i => i.id);
|
||||
this.messageIds = m.map(i => i.id);
|
||||
}
|
||||
|
||||
// Task lifecycle
|
||||
|
@ -215,50 +65,19 @@ export default class ChangeMailTask extends Task {
|
|||
}
|
||||
|
||||
createIdenticalTask() {
|
||||
const task = new this.constructor(this);
|
||||
|
||||
// Never give the undo task the Model objects - make it look them up!
|
||||
// This ensures that they never revert other fields
|
||||
const toIds = (arr) => _.map(arr, v => (_.isString(v) ? v : v.id));
|
||||
task.threads = toIds(this.threads);
|
||||
task.messages = (this.threads.length > 0) ? [] : toIds(this.messages);
|
||||
return task;
|
||||
return new this.constructor(this);
|
||||
}
|
||||
|
||||
objectIds() {
|
||||
return [].concat(this.threads, this.messages).map((v) =>
|
||||
(_.isString(v) ? v : v.id)
|
||||
);
|
||||
return [].concat(this.threadIds, this.messageIds);
|
||||
}
|
||||
|
||||
objectClass() {
|
||||
return (this.threads && this.threads.length) ? Thread : Message;
|
||||
}
|
||||
|
||||
objectArray() {
|
||||
return (this.threads && this.threads.length) ? this.threads : this.messages;
|
||||
return (this.threadIds && this.threadIds.length) ? Thread : Message;
|
||||
}
|
||||
|
||||
numberOfImpactedItems() {
|
||||
return this.objectArray().length;
|
||||
}
|
||||
|
||||
// Helpers used in subclasses
|
||||
|
||||
_lockAll() {
|
||||
const klass = this.objectClass();
|
||||
this._locked = this._locked || {};
|
||||
for (const item of this.objectArray()) {
|
||||
this._locked[item.id] = this._locked[item.id] || 0;
|
||||
this._locked[item.id] += 1;
|
||||
NylasAPI.incrementRemoteChangeLock(klass, item.id);
|
||||
}
|
||||
}
|
||||
|
||||
_removeLock(item) {
|
||||
const klass = this.objectClass();
|
||||
NylasAPI.decrementRemoteChangeLock(klass, item.id);
|
||||
this._locked[item.id] -= 1;
|
||||
return this.objectIds().length;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class ChangeStarredTask extends ChangeMailTask {
|
|||
}
|
||||
|
||||
description() {
|
||||
const count = this.threads.length;
|
||||
const count = this.threadIds.length;
|
||||
const type = count > 1 ? "threads" : "thread";
|
||||
|
||||
if (this._isUndoTask) {
|
||||
|
@ -32,7 +32,7 @@ export default class ChangeStarredTask extends ChangeMailTask {
|
|||
}
|
||||
|
||||
performLocal() {
|
||||
if (this.threads.length === 0) {
|
||||
if (this.threadIds.length === 0) {
|
||||
return Promise.reject(new Error("ChangeStarredTask: You must provide a `threads` Array of models or IDs."));
|
||||
}
|
||||
return super.performLocal();
|
||||
|
@ -45,26 +45,9 @@ export default class ChangeStarredTask extends ChangeMailTask {
|
|||
const eventName = this.unread ? "Starred" : "Unstarred";
|
||||
Actions.recordUserEvent(`Threads ${eventName}`, {
|
||||
source: this.source,
|
||||
numThreads: this.threads.length,
|
||||
numThreads: this.threadIds.length,
|
||||
description: this.description(),
|
||||
isUndo: this._isUndoTask,
|
||||
})
|
||||
}
|
||||
|
||||
retrieveModels() {
|
||||
return Promise.props({
|
||||
threads: DatabaseStore.modelify(Thread, this.threads),
|
||||
}).then(({threads}) => {
|
||||
this.threads = _.compact(threads);
|
||||
return Promise.resolve();
|
||||
})
|
||||
}
|
||||
|
||||
changesToModel(model) {
|
||||
return {starred: this.starred};
|
||||
}
|
||||
|
||||
requestBodyForModel(model) {
|
||||
return {starred: model.starred};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export default class ChangeUnreadTask extends ChangeMailTask {
|
|||
}
|
||||
|
||||
description() {
|
||||
const count = this.threads.length;
|
||||
const count = this.threadIds.length;
|
||||
const type = count > 1 ? 'threads' : 'thread';
|
||||
|
||||
if (this._isUndoTask) {
|
||||
|
@ -38,47 +38,4 @@ export default class ChangeUnreadTask extends ChangeMailTask {
|
|||
}
|
||||
return this._canBeUndone
|
||||
}
|
||||
|
||||
performLocal() {
|
||||
if (this.threads.length === 0) {
|
||||
return Promise.reject(new Error("ChangeUnreadTask: You must provide a `threads` Array of models or IDs."))
|
||||
}
|
||||
return super.performLocal();
|
||||
}
|
||||
|
||||
recordUserEvent() {
|
||||
if (this.source === "Mail Rules") {
|
||||
return
|
||||
}
|
||||
const eventName = this.unread ? "Unread" : "Read";
|
||||
Actions.recordUserEvent(`Threads Marked as ${eventName}`, {
|
||||
source: this.source,
|
||||
numThreads: this.threads.length,
|
||||
description: this.description(),
|
||||
isUndo: this._isUndoTask,
|
||||
})
|
||||
}
|
||||
|
||||
retrieveModels() {
|
||||
// Convert arrays of IDs or models to models.
|
||||
// modelify returns immediately if (no work is required)
|
||||
return Promise.props({
|
||||
threads: DatabaseStore.modelify(Thread, this.threads),
|
||||
}).then(({threads}) => {
|
||||
this.threads = _.compact(threads);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
processNestedMessages() {
|
||||
return true;
|
||||
}
|
||||
|
||||
changesToModel(model) {
|
||||
return {unread: this.unread};
|
||||
}
|
||||
|
||||
requestBodyForModel(model) {
|
||||
return {unread: model.unread};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint no-unused-vars: 0*/
|
||||
import _ from 'underscore';
|
||||
import Model from '../models/model';
|
||||
import Attributes from '../attributes';
|
||||
import {generateTempId} from '../models/utils';
|
||||
import {PermanentErrorCodes} from '../nylas-api';
|
||||
import {APIError} from '../errors';
|
||||
|
@ -13,10 +14,23 @@ const TaskStatus = {
|
|||
};
|
||||
|
||||
export default class Task extends Model {
|
||||
|
||||
static Status = TaskStatus;
|
||||
static SubclassesUseModelTable = Task;
|
||||
|
||||
static attributes = Object.assign({}, Model.attributes, {
|
||||
version: Attributes.String({
|
||||
queryable: true,
|
||||
modelKey: 'version',
|
||||
}),
|
||||
status: Attributes.String({
|
||||
queryable: true,
|
||||
modelKey: 'status',
|
||||
}),
|
||||
accountId: Attributes.String({
|
||||
modelKey: 'accountId',
|
||||
}),
|
||||
});
|
||||
|
||||
// Public: Override the constructor to pass initial args to your Task and
|
||||
// initialize instance variables.
|
||||
//
|
||||
|
@ -26,147 +40,11 @@ export default class Task extends Model {
|
|||
// On construction, all Tasks instances are given a unique `id`.
|
||||
constructor() {
|
||||
super();
|
||||
this.version = 1;
|
||||
this._rememberedToCallSuper = true;
|
||||
this.id = generateTempId();
|
||||
this.accountId = "1"; //TODO BG hack
|
||||
this.sequentialId = null; // set when queued
|
||||
this.queueState = {
|
||||
isProcessing: false,
|
||||
localError: null,
|
||||
localComplete: false,
|
||||
remoteError: null,
|
||||
remoteAttempts: 0,
|
||||
remoteComplete: false,
|
||||
status: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Private: This is a internal wrapper around `performLocal`
|
||||
runLocal() {
|
||||
if (!this._rememberedToCallSuper) {
|
||||
throw new Error("Your must call `super` from your Task's constructors");
|
||||
}
|
||||
|
||||
if (this.queueState.localComplete) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
return this.performLocal()
|
||||
.then(() => {
|
||||
this.queueState.localComplete = true;
|
||||
this.queueState.localError = null;
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(this._handleLocalError);
|
||||
} catch (err) {
|
||||
return this._handleLocalError(err);
|
||||
}
|
||||
}
|
||||
|
||||
_handleLocalError = (err) => {
|
||||
this.queueState.localError = err;
|
||||
this.queueState.status = Task.Status.Failed;
|
||||
NylasEnv.reportError(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
|
||||
// HELPER METHODS
|
||||
validateRequiredFields = (fields = []) => {
|
||||
for (const field of fields) {
|
||||
if (!this[field]) {
|
||||
throw new Error(`Must pass ${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// METHODS TO OBSERVE
|
||||
//
|
||||
// Public: **Required** | Override to perform local, optimistic updates.
|
||||
//
|
||||
// Most tasks will put code in here that updates the {DatabaseStore}
|
||||
//
|
||||
// You should also implement the rollback behavior inside of
|
||||
// `performLocal` or in some helper method. It's common practice (but not
|
||||
// automatic) for `performLocal` to be re-called at the end of an API
|
||||
// failure from `performRemote`.
|
||||
//
|
||||
// That rollback behavior is also likely the same when you want to undo a
|
||||
// task. It's common practice (but not automatic) for `createUndoTask` to
|
||||
// set some flag that `performLocal` will recognize to implement the
|
||||
// rollback behavior.
|
||||
//
|
||||
// `performLocal` will complete BEFORE the task actually enters the
|
||||
// {TaskQueue}.
|
||||
//
|
||||
// if (you would like to do work after `performLocal` has run, you can use)
|
||||
// {TaskQueue::waitForPerformLocal}. Pass it the task and it
|
||||
// will return a Promise that resolves once the local action has
|
||||
// completed. This is contained in the {TaskQueue} so you can
|
||||
// listen to tasks across windows.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ### Simple Optimistic Updating
|
||||
//
|
||||
// ```js
|
||||
// class MyTask extends Task {
|
||||
// performLocal() {
|
||||
// this.updatedModel = this._myModelUpdateCode()
|
||||
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ### Handling rollback on API failure
|
||||
//
|
||||
// ```js
|
||||
// class MyTask extends Task
|
||||
// performLocal() {
|
||||
// if (this._reverting) {
|
||||
// this.updatedModel = this._myModelRollbackCode();
|
||||
// } else {
|
||||
// this.updatedModel = this._myModelUpdateCode();
|
||||
// }
|
||||
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
|
||||
// }
|
||||
// performRemote() {
|
||||
// return this._APIPutHelperMethod(this.updatedModel).catch((apiError) => {
|
||||
// if (apiError.statusCode === 500) {
|
||||
// this._reverting = true;
|
||||
// return this.performLocal();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ### Handling an undo task
|
||||
//
|
||||
// ```js
|
||||
// class MyTask extends Task {
|
||||
// performLocal() {
|
||||
// if (this._isUndoTask) {
|
||||
// this.updatedModel = this._myModelRollbackCode();
|
||||
// } else {
|
||||
// this.updatedModel = this._myModelUpdateCode();
|
||||
// }
|
||||
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
|
||||
// }
|
||||
//
|
||||
// createUndoTask() {
|
||||
// undoTask = this.createIdenticalTask();
|
||||
// undoTask._isUndoTask = true;
|
||||
// return undoTask;
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Also see the documentation on the required undo methods
|
||||
//
|
||||
// Returns a {Promise} that resolves when your updates are complete.
|
||||
performLocal() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Public: It's up to you to determine how you want to indicate whether
|
||||
|
@ -196,7 +74,7 @@ export default class Task extends Model {
|
|||
// Public: Return a deep-cloned task to be used for an undo task
|
||||
createIdenticalTask() {
|
||||
const json = this.toJSON();
|
||||
delete json.queueState;
|
||||
delete json.status;
|
||||
return (new this.constructor()).fromJSON(json);
|
||||
}
|
||||
|
||||
|
@ -224,7 +102,7 @@ export default class Task extends Model {
|
|||
|
||||
// Private: Allows for serialization of tasks
|
||||
toJSON() {
|
||||
return this;
|
||||
return Object.assign(super.toJSON(), this);
|
||||
}
|
||||
|
||||
// Private: Allows for deserialization of tasks
|
||||
|
|
Loading…
Reference in a new issue