From 73f82ec079929dcef9552f9dca8cfda2c18d67b6 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 21 Jun 2017 15:16:48 -0700 Subject: [PATCH] Remove Category concept, part 1 --- .../client-app/src/flux/models/folder.es6 | 190 +++++++++++++++++- packages/client-app/src/flux/models/label.es6 | 189 ++++++++++++++++- .../client-app/src/flux/models/thread.es6 | 67 ++---- .../src/global/nylas-observables.coffee | 32 ++- .../client-app/src/mailbox-perspective.coffee | 13 +- 5 files changed, 422 insertions(+), 69 deletions(-) diff --git a/packages/client-app/src/flux/models/folder.es6 b/packages/client-app/src/flux/models/folder.es6 index 7134672a9..afabdb3a6 100644 --- a/packages/client-app/src/flux/models/folder.es6 +++ b/packages/client-app/src/flux/models/folder.es6 @@ -1,2 +1,188 @@ -import Category from './category'; -export default Category; +/* eslint global-require: 0 */ +import {FolderSyncProgressStore} from 'nylas-exports'; +import Model from './model'; +import Attributes from '../attributes'; +let AccountStore = null + +// We look for a few standard categories and display them in the Mailboxes +// portion of the left sidebar. Note that these may not all be present on +// a particular account. +const ToObject = (arr) => { + return arr.reduce((o, v) => { + o[v] = v; + return o; + }, {}); +} + +const StandardCategories = ToObject([ + "inbox", + "important", + "sent", + "drafts", + "all", + "spam", + "archive", + "trash", +]); + +const LockedCategories = ToObject([ + "sent", + "drafts", + "N1-Snoozed", +]); + +const HiddenCategories = ToObject([ + "sent", + "drafts", + "all", + "archive", + "starred", + "important", + "N1-Snoozed", +]); + +/** +Private: +This abstract class has only two concrete implementations: + - `Folder` + - `Label` + +See the equivalent models for details. + +Folders and Labels have different semantics. The `Category` class only exists to help DRY code where they happen to behave the same + +## Attributes + +`name`: {AttributeString} The internal name of the label or folder. Queryable. + +`displayName`: {AttributeString} The display-friendly name of the label or folder. Queryable. + +Section: Models +*/ +export default class Folder extends Model { + + get displayName() { + if (this.path && this.path.startsWith('INBOX.')) { + return this.path.substr(6); + } + if (this.path && this.path === 'INBOX') { + return 'Inbox'; + } + return this.path; + } + + get name() { + return this.role; + } + + static attributes = Object.assign({}, Model.attributes, { + role: Attributes.String({ + queryable: true, + modelKey: 'role', + }), + path: Attributes.String({ + queryable: true, + modelKey: 'path', + }), + imapName: Attributes.String({ + modelKey: 'imapName', + jsonKey: 'imap_name', + }), + syncProgress: Attributes.Object({ + modelKey: 'syncProgress', + jsonKey: 'sync_progress', + }), + }); + + static Types = { + Standard: 'standard', + Locked: 'locked', + User: 'user', + Hidden: 'hidden', + } + + static StandardCategoryNames = Object.keys(StandardCategories) + static LockedCategoryNames = Object.keys(LockedCategories) + static HiddenCategoryNames = Object.keys(HiddenCategories) + + static categoriesSharedName(cats) { + if (!cats || cats.length === 0) { + return null; + } + const name = cats[0].name + if (!cats.every((cat) => cat.name === name)) { + return null; + } + return name; + } + + static additionalSQLiteConfig = { + setup: () => { + return [ + // 'CREATE INDEX IF NOT EXISTS FolderNameIndex ON Folder(accountId,name)', + // 'CREATE UNIQUE INDEX IF NOT EXISTS FolderClientIndex ON Folder(id)', + ]; + }, + }; + + displayType() { + return 'folder'; + } + + hue() { + if (!this.displayName) { + return 0; + } + + let hue = 0; + for (let i = 0; i < this.displayName.length; i++) { + hue += this.displayName.charCodeAt(i); + } + hue *= (396.0 / 512.0); + return hue; + } + + isStandardCategory(forceShowImportant) { + let showImportant = forceShowImportant; + if (showImportant === undefined) { + showImportant = NylasEnv.config.get('core.workspace.showImportant'); + } + if (showImportant === true) { + return !!StandardCategories[this.name]; + } + return !!StandardCategories[this.name] && (this.name !== 'important'); + } + + isLockedCategory() { + return !!LockedCategories[this.name] || !!LockedCategories[this.displayName]; + } + + isHiddenCategory() { + return !!HiddenCategories[this.name] || !!HiddenCategories[this.displayName]; + } + + isUserCategory() { + return !this.isStandardCategory() && !this.isHiddenCategory(); + } + + isInbox() { + return this.name === 'inbox' + } + + isArchive() { + return ['all', 'archive'].includes(this.name); + } + + isSyncComplete() { + // We sync by folders, not labels. If the category is a label, or hasn't been + // assigned an object type yet, just return based on the sync status for the + // entire account. + if (this.object !== 'folder') { + return FolderSyncProgressStore.isSyncCompleteForAccount(this.accountId); + } + return FolderSyncProgressStore.isSyncCompleteForAccount( + this.accountId, + this.name || this.displayName + ); + } +} diff --git a/packages/client-app/src/flux/models/label.es6 b/packages/client-app/src/flux/models/label.es6 index 7134672a9..5fbf1b343 100644 --- a/packages/client-app/src/flux/models/label.es6 +++ b/packages/client-app/src/flux/models/label.es6 @@ -1,2 +1,187 @@ -import Category from './category'; -export default Category; +/* eslint global-require: 0 */ +import {FolderSyncProgressStore} from 'nylas-exports'; +import Model from './model'; +import Attributes from '../attributes'; +let AccountStore = null + +// We look for a few standard categories and display them in the Mailboxes +// portion of the left sidebar. Note that these may not all be present on +// a particular account. +const ToObject = (arr) => { + return arr.reduce((o, v) => { + o[v] = v; + return o; + }, {}); +} + +const StandardCategories = ToObject([ + "inbox", + "important", + "sent", + "drafts", + "all", + "spam", + "archive", + "trash", +]); + +const LockedCategories = ToObject([ + "sent", + "drafts", + "N1-Snoozed", +]); + +const HiddenCategories = ToObject([ + "sent", + "drafts", + "all", + "archive", + "starred", + "important", + "N1-Snoozed", +]); + +/** +Private: +This abstract class has only two concrete implementations: + - `Folder` + - `Label` + +See the equivalent models for details. + +Folders and Labels have different semantics. The `Category` class only exists to help DRY code where they happen to behave the same + +## Attributes + +`name`: {AttributeString} The internal name of the label or folder. Queryable. + +`displayName`: {AttributeString} The display-friendly name of the label or folder. Queryable. + +Section: Models +*/ +export default class Label extends Model { + + static attributes = Object.assign({}, Model.attributes, { + name: Attributes.String({ + queryable: true, + modelKey: 'name', + }), + displayName: Attributes.String({ + queryable: true, + modelKey: 'displayName', + jsonKey: 'display_name', + }), + imapName: Attributes.String({ + modelKey: 'imapName', + jsonKey: 'imap_name', + }), + syncProgress: Attributes.Object({ + modelKey: 'syncProgress', + jsonKey: 'sync_progress', + }), + }); + + static Types = { + Standard: 'standard', + Locked: 'locked', + User: 'user', + Hidden: 'hidden', + } + + static StandardCategoryNames = Object.keys(StandardCategories) + static LockedCategoryNames = Object.keys(LockedCategories) + static HiddenCategoryNames = Object.keys(HiddenCategories) + + static categoriesSharedName(cats) { + if (!cats || cats.length === 0) { + return null; + } + const name = cats[0].name + if (!cats.every((cat) => cat.name === name)) { + return null; + } + return name; + } + + static additionalSQLiteConfig = { + setup: () => { + return [ + // 'CREATE INDEX IF NOT EXISTS LabelNameIndex ON Label(accountId,name)', + // 'CREATE UNIQUE INDEX IF NOT EXISTS LabelClientIndex ON Label(id)', + ]; + }, + }; + + fromJSON(json) { + super.fromJSON(json); + + if (this.displayName && this.displayName.startsWith('INBOX.')) { + this.displayName = this.displayName.substr(6); + } + if (this.displayName && this.displayName === 'INBOX') { + this.displayName = 'Inbox'; + } + return this; + } + + displayType() { + return 'label'; + } + + hue() { + if (!this.displayName) { + return 0; + } + + let hue = 0; + for (let i = 0; i < this.displayName.length; i++) { + hue += this.displayName.charCodeAt(i); + } + hue *= (396.0 / 512.0); + return hue; + } + + isStandardCategory(forceShowImportant) { + let showImportant = forceShowImportant; + if (showImportant === undefined) { + showImportant = NylasEnv.config.get('core.workspace.showImportant'); + } + if (showImportant === true) { + return !!StandardCategories[this.name]; + } + return !!StandardCategories[this.name] && (this.name !== 'important'); + } + + isLockedCategory() { + return !!LockedCategories[this.name] || !!LockedCategories[this.displayName]; + } + + isHiddenCategory() { + return !!HiddenCategories[this.name] || !!HiddenCategories[this.displayName]; + } + + isUserCategory() { + return !this.isStandardCategory() && !this.isHiddenCategory(); + } + + isInbox() { + return this.name === 'inbox' + } + + isArchive() { + return ['all', 'archive'].includes(this.name); + } + + isSyncComplete() { + // We sync by folders, not labels. If the category is a label, or hasn't been + // assigned an object type yet, just return based on the sync status for the + // entire account. + if (this.object !== 'folder') { + return FolderSyncProgressStore.isSyncCompleteForAccount(this.accountId); + } + return FolderSyncProgressStore.isSyncCompleteForAccount( + this.accountId, + this.name || this.displayName + ); + } +} diff --git a/packages/client-app/src/flux/models/thread.es6 b/packages/client-app/src/flux/models/thread.es6 index b23a22ca5..129ca66cd 100644 --- a/packages/client-app/src/flux/models/thread.es6 +++ b/packages/client-app/src/flux/models/thread.es6 @@ -1,7 +1,8 @@ import _ from 'underscore' import Message from './message' import Contact from './contact' -import Category from './category' +import Folder from './folder' +import Label from './label' import Attributes from '../attributes' import DatabaseStore from '../stores/database-store' import ModelWithMetadata from './model-with-metadata' @@ -64,16 +65,20 @@ class Thread extends ModelWithMetadata { modelKey: 'version', }), - categories: Attributes.Collection({ + folders: Attributes.Collection({ queryable: true, - modelKey: 'categories', + modelKey: 'folders', joinOnField: 'id', joinQueryableBy: ['inAllMail', 'lastMessageReceivedTimestamp', 'lastMessageSentTimestamp', 'unread'], - itemClass: Category, + itemClass: Folder, }), - categoriesType: Attributes.String({ - modelKey: 'categoriesType', + labels: Attributes.Collection({ + queryable: true, + modelKey: 'labels', + joinOnField: 'id', + joinQueryableBy: ['inAllMail', 'lastMessageReceivedTimestamp', 'lastMessageSentTimestamp', 'unread'], + itemClass: Label, }), participants: Attributes.Collection({ @@ -188,35 +193,8 @@ class Thread extends ModelWithMetadata { }) } - get labels() { - return this.categories; - } - - set labels(labels) { - this.categories = labels; - } - - get folders() { - return this.categories; - } - - set folders(folders) { - this.categories = folders; - } - get inAllMail() { - if (this.categoriesType === 'labels') { - const inAllMail = _.any(this.categories, cat => cat.name === 'all') - if (inAllMail) { - return true; - } - const inTrashOrSpam = _.any(this.categories, cat => cat.name === 'trash' || cat.name === 'spam') - if (!inTrashOrSpam) { - return true; - } - return false - } - return true + return this.folders.find(f => f.role === 'all'); } /** @@ -229,17 +207,6 @@ class Thread extends ModelWithMetadata { fromJSON(json) { super.fromJSON(json) - if (json.folders) { - this.categoriesType = 'folders' - this.categories = Thread.attributes.categories.fromJSON(json.folders) - } - - if (json.labels && json.labels.length > 0) { - this.categoriesType = 'labels' - if (!this.categories) this.categories = []; - this.categories = this.categories.concat(Thread.attributes.categories.fromJSON(json.labels)) - } - ['participants'].forEach((attr) => { const value = this[attr] if (!(value && value instanceof Array)) { @@ -296,14 +263,4 @@ class Thread extends ModelWithMetadata { } } -Object.defineProperty(Thread.attributes, "labels", { - enumerable: false, - get: () => Thread.attributes.categories, -}) - -Object.defineProperty(Thread.attributes, "folders", { - enumerable: false, - get: () => Thread.attributes.categories, -}) - export default Thread; diff --git a/packages/client-app/src/global/nylas-observables.coffee b/packages/client-app/src/global/nylas-observables.coffee index 2aaea8eee..d57f72941 100644 --- a/packages/client-app/src/global/nylas-observables.coffee +++ b/packages/client-app/src/global/nylas-observables.coffee @@ -1,6 +1,7 @@ Rx = require 'rx-lite' _ = require 'underscore' -Category = require('../flux/models/category').default +Folder = require('../flux/models/folder').default +Label = require('../flux/models/label').default QuerySubscriptionPool = require('../flux/models/query-subscription-pool').default DatabaseStore = require('../flux/stores/database-store').default @@ -27,17 +28,32 @@ CategoryOperators = CategoryObservables = forAllAccounts: => - observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category)) - _.extend(observable, CategoryOperators) - observable + folders = Rx.Observable.fromQuery(DatabaseStore.findAll(Folder)) + labels = Rx.Observable.fromQuery(DatabaseStore.findAll(Label)) + joined = Rx.Observable.combineLatest(folders, labels, (f, l) => + debugger + [].concat(f, l) + ) + _.extend(joined, CategoryOperators) + joined forAccount: (account) => if account - observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category).where(accountId: account.id)) + folders = Rx.Observable.fromQuery(DatabaseStore.findAll(Folder).where(accountId: account.id)) + labels = Rx.Observable.fromQuery(DatabaseStore.findAll(Label).where(accountId: account.id)) + joined = Rx.Observable.combineLatest(folders, labels, (f, l) => + debugger + [].concat(f, l) + ) else - observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category)) - _.extend(observable, CategoryOperators) - observable + folders = Rx.Observable.fromQuery(DatabaseStore.findAll(Folder)) + labels = Rx.Observable.fromQuery(DatabaseStore.findAll(Label)) + joined = Rx.Observable.combineLatest(folders, labels, (f, l) => + debugger + [].concat(f, l) + ) + _.extend(joined, CategoryOperators) + joined standard: (account) => observable = Rx.Observable.fromConfig('core.workspace.showImportant') diff --git a/packages/client-app/src/mailbox-perspective.coffee b/packages/client-app/src/mailbox-perspective.coffee index ec7c7d55a..cb71c3785 100644 --- a/packages/client-app/src/mailbox-perspective.coffee +++ b/packages/client-app/src/mailbox-perspective.coffee @@ -13,6 +13,8 @@ UnreadQuerySubscription = require('./flux/models/unread-query-subscription').def Matcher = require('./flux/attributes/matcher').default Thread = require('./flux/models/thread').default Category = require('./flux/models/category').default +Folder = require('./flux/models/folder').default +Label = require('./flux/models/label').default Actions = require('./flux/actions').default ChangeUnreadTask = null @@ -268,9 +270,16 @@ class CategoryMailboxPerspective extends MailboxPerspective super(other) and _.isEqual(_.pluck(@categories(), 'id'), _.pluck(other.categories(), 'id')) threads: => + folders = @categories().filter((c) => c instanceof Folder) + labels = @categories().filter((c) => c instanceof Label) query = DatabaseStore.findAll(Thread) - .where([Thread.attributes.categories.containsAny(_.pluck(@categories(), 'id'))]) - .limit(0) + + if folders.length > 0 + query = query.where([Thread.attributes.folders.containsAny(_.pluck(folders, 'id'))]) + if labels.length > 0 + query = query.where([Thread.attributes.labels.containsAny(_.pluck(labels, 'id'))]) + + query = query.limit(0) if @isSent() query.order(Thread.attributes.lastMessageSentTimestamp.descending())