diff --git a/packages/client-app/internal_packages/category-picker/lib/category-picker-popover.jsx b/packages/client-app/internal_packages/category-picker/lib/category-picker-popover.jsx
index b46d182e1..6e71d775d 100644
--- a/packages/client-app/internal_packages/category-picker/lib/category-picker-popover.jsx
+++ b/packages/client-app/internal_packages/category-picker/lib/category-picker-popover.jsx
@@ -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 {
)
diff --git a/packages/client-app/spec/models/category-spec.coffee b/packages/client-app/spec/models/category-spec.coffee
index 0b838e267..b4d539d02 100644
--- a/packages/client-app/spec/models/category-spec.coffee
+++ b/packages/client-app/spec/models/category-spec.coffee
@@ -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', ->
diff --git a/packages/client-app/spec/models/thread-spec.coffee b/packages/client-app/spec/models/thread-spec.coffee
index 95fa20eab..a4a06e82b 100644
--- a/packages/client-app/spec/models/thread-spec.coffee
+++ b/packages/client-app/spec/models/thread-spec.coffee
@@ -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)
diff --git a/packages/client-app/spec/tasks/destroy-category-task-spec.coffee b/packages/client-app/spec/tasks/destroy-category-task-spec.coffee
index 6f1c33c58..9faf6be94 100644
--- a/packages/client-app/spec/tasks/destroy-category-task-spec.coffee
+++ b/packages/client-app/spec/tasks/destroy-category-task-spec.coffee
@@ -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 {
diff --git a/packages/client-app/src/flux/action-bridge-cpp.es6 b/packages/client-app/src/flux/action-bridge-cpp.es6
index 132ff18e5..60be9563e 100644
--- a/packages/client-app/src/flux/action-bridge-cpp.es6
+++ b/packages/client-app/src/flux/action-bridge-cpp.es6
@@ -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
diff --git a/packages/client-app/src/flux/models/category.es6 b/packages/client-app/src/flux/models/category.es6
index 160db8985..5f9c68241 100644
--- a/packages/client-app/src/flux/models/category.es6
+++ b/packages/client-app/src/flux/models/category.es6
@@ -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 = {
diff --git a/packages/client-app/src/flux/models/message.es6 b/packages/client-app/src/flux/models/message.es6
index cc661b90a..78f5335a2 100644
--- a/packages/client-app/src/flux/models/message.es6
+++ b/packages/client-app/src/flux/models/message.es6
@@ -147,6 +147,10 @@ export default class Message extends ModelWithMetadata {
modelKey: 'subject',
}),
+ folderImapUID: Attributes.Number({
+ modelKey: 'folderImapUID',
+ }),
+
draft: Attributes.Boolean({
modelKey: 'draft',
queryable: true,
diff --git a/packages/client-app/src/flux/stores/recently-read-store.es6 b/packages/client-app/src/flux/stores/recently-read-store.es6
index c60e12f1a..23d5d2e14 100644
--- a/packages/client-app/src/flux/stores/recently-read-store.es6
+++ b/packages/client-app/src/flux/stores/recently-read-store.es6
@@ -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;
});
diff --git a/packages/client-app/src/flux/stores/task-queue.es6 b/packages/client-app/src/flux/stores/task-queue.es6
index 5ab4734fe..fbbcaebef 100644
--- a/packages/client-app/src/flux/stores/task-queue.es6
+++ b/packages/client-app/src/flux/stores/task-queue.es6
@@ -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());
}
diff --git a/packages/client-app/src/flux/tasks/change-folder-task.es6 b/packages/client-app/src/flux/tasks/change-folder-task.es6
index 1b6dcc7d5..4ba4a2ecf 100644
--- a/packages/client-app/src/flux/tasks/change-folder-task.es6
+++ b/packages/client-app/src/flux/tasks/change-folder-task.es6
@@ -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;
- }
}
diff --git a/packages/client-app/src/flux/tasks/change-labels-task.es6 b/packages/client-app/src/flux/tasks/change-labels-task.es6
index c0deecd2e..e782a6583 100644
--- a/packages/client-app/src/flux/tasks/change-labels-task.es6
+++ b/packages/client-app/src/flux/tasks/change-labels-task.es6
@@ -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};
- }
}
diff --git a/packages/client-app/src/flux/tasks/change-mail-task.es6 b/packages/client-app/src/flux/tasks/change-mail-task.es6
index a81a074be..abf42a098 100644
--- a/packages/client-app/src/flux/tasks/change-mail-task.es6
+++ b/packages/client-app/src/flux/tasks/change-mail-task.es6
@@ -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;
}
}
diff --git a/packages/client-app/src/flux/tasks/change-starred-task.es6 b/packages/client-app/src/flux/tasks/change-starred-task.es6
index 5d64f3fe0..1d46b6eb7 100644
--- a/packages/client-app/src/flux/tasks/change-starred-task.es6
+++ b/packages/client-app/src/flux/tasks/change-starred-task.es6
@@ -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};
- }
}
diff --git a/packages/client-app/src/flux/tasks/change-unread-task.es6 b/packages/client-app/src/flux/tasks/change-unread-task.es6
index eb75da23d..6a4c69d2d 100644
--- a/packages/client-app/src/flux/tasks/change-unread-task.es6
+++ b/packages/client-app/src/flux/tasks/change-unread-task.es6
@@ -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};
- }
}
diff --git a/packages/client-app/src/flux/tasks/task.es6 b/packages/client-app/src/flux/tasks/task.es6
index 457ff1336..82a9b11de 100644
--- a/packages/client-app/src/flux/tasks/task.es6
+++ b/packages/client-app/src/flux/tasks/task.es6
@@ -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