diff --git a/app/internal_packages/activity-list/specs/activity-list-spec.jsx b/app/internal_packages/activity-list/specs/activity-list-spec.jsx index 4790eed20..3a4b7ab8e 100644 --- a/app/internal_packages/activity-list/specs/activity-list-spec.jsx +++ b/app/internal_packages/activity-list/specs/activity-list-spec.jsx @@ -69,7 +69,7 @@ let pluginValue = { timestamp: 1461361759.351055, }], }; -messages[0].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue); +messages[0].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue); pluginValue = { links: [{ click_count: 1, @@ -79,24 +79,24 @@ pluginValue = { }], tracked: true, }; -messages[0].applyPluginMetadata(LINK_TRACKING_ID, pluginValue); +messages[0].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue); pluginValue = { open_count: 1, open_data: [{ timestamp: 1461361763.283720, }], }; -messages[1].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue); +messages[1].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue); pluginValue = { links: [], tracked: false, }; -messages[1].applyPluginMetadata(LINK_TRACKING_ID, pluginValue); +messages[1].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue); pluginValue = { open_count: 0, open_data: [], }; -messages[2].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue); +messages[2].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue); pluginValue = { links: [{ click_count: 0, @@ -104,7 +104,7 @@ pluginValue = { }], tracked: true, }; -messages[2].applyPluginMetadata(LINK_TRACKING_ID, pluginValue); +messages[2].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue); describe('ActivityList', function activityList() { diff --git a/app/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 b/app/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 index 3e6213235..1389b558b 100644 --- a/app/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 +++ b/app/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 @@ -36,33 +36,34 @@ function forEachATagInBody(draftBodyRootNode, callback) { export default class LinkTrackingComposerExtension extends ComposerExtension { static applyTransformsForSending({draftBodyRootNode, draft}) { const metadata = draft.metadataForPluginId(PLUGIN_ID); - if (metadata) { - const messageUid = draft.clientId; - const links = []; + if (!metadata) { + return; + } + const messageUid = draft.clientId; + const links = []; - forEachATagInBody(draftBodyRootNode, (el) => { - const url = el.getAttribute('href'); - if (!RegExpUtils.urlRegex().test(url)) { - return; - } - const encoded = encodeURIComponent(url); - const redirectUrl = `${PLUGIN_URL}/link/${draft.headerMessageId}/${links.length}?redirect=${encoded}`; + forEachATagInBody(draftBodyRootNode, (el) => { + const url = el.getAttribute('href'); + if (!RegExpUtils.urlRegex().test(url)) { + return; + } + const encoded = encodeURIComponent(url); + const redirectUrl = `${PLUGIN_URL}/link/${draft.headerMessageId}/${links.length}?redirect=${encoded}`; - links.push({ - url, - click_count: 0, - click_data: [], - redirect_url: redirectUrl, - }); - - el.setAttribute('href', redirectUrl); + links.push({ + url, + click_count: 0, + click_data: [], + redirect_url: redirectUrl, }); - // save the link info to draft metadata - metadata.uid = messageUid; - metadata.links = links; - draft.applyPluginMetadata(PLUGIN_ID, metadata); - } + el.setAttribute('href', redirectUrl); + }); + + // save the link info to draft metadata + metadata.uid = messageUid; + metadata.links = links; + draft.directlyAttachMetadata(PLUGIN_ID, metadata); } static unapplyTransformsForSending({draftBodyRootNode}) { diff --git a/app/internal_packages/link-tracking/specs/link-tracking-composer-extension-spec.es6 b/app/internal_packages/link-tracking/specs/link-tracking-composer-extension-spec.es6 index 6bd36673b..16dab2034 100644 --- a/app/internal_packages/link-tracking/specs/link-tracking-composer-extension-spec.es6 +++ b/app/internal_packages/link-tracking/specs/link-tracking-composer-extension-spec.es6 @@ -49,7 +49,7 @@ xdescribe('Link tracking composer extension', function linkTrackingComposerExten describe("With properly formatted metadata and correct params", () => { beforeEach(() => { this.metadata = {tracked: true}; - this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata); + this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata); }); it("replaces links in the unquoted portion of the body", () => { @@ -84,7 +84,7 @@ xdescribe('Link tracking composer extension', function linkTrackingComposerExten beforeEach(() => { this.metadata = {tracked: true, uid: '123'}; this.draft = new Message({accountId: "test"}); - this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata); + this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata); }); it("takes no action if there are no tracked links in the body", () => { diff --git a/app/internal_packages/message-list/lib/message-item-container.jsx b/app/internal_packages/message-list/lib/message-item-container.jsx index 557d604f4..1f59b309b 100644 --- a/app/internal_packages/message-list/lib/message-item-container.jsx +++ b/app/internal_packages/message-list/lib/message-item-container.jsx @@ -16,7 +16,7 @@ export default class MessageItemContainer extends React.Component { message: React.PropTypes.object.isRequired, messages: React.PropTypes.array.isRequired, collapsed: React.PropTypes.bool, - isLastItem: React.PropTypes.bool, + isMostRecent: React.PropTypes.bool, isBeforeReplyArea: React.PropTypes.bool, scrollTo: React.PropTypes.func, }; @@ -82,7 +82,7 @@ export default class MessageItemContainer extends React.Component { messages={this.props.messages} className={this._classNames()} collapsed={this.props.collapsed} - isLastItem={this.props.isLastItem} + isMostRecent={this.props.isMostRecent} /> ); } diff --git a/app/internal_packages/message-list/lib/message-item.jsx b/app/internal_packages/message-list/lib/message-item.jsx index a1ca093ad..3dda4c283 100644 --- a/app/internal_packages/message-list/lib/message-item.jsx +++ b/app/internal_packages/message-list/lib/message-item.jsx @@ -27,7 +27,7 @@ export default class MessageItem extends React.Component { messages: PropTypes.array.isRequired, collapsed: PropTypes.bool, pending: PropTypes.bool, - isLastItem: PropTypes.bool, + isMostRecent: PropTypes.bool, className: PropTypes.string, }; @@ -107,7 +107,7 @@ export default class MessageItem extends React.Component { } _onToggleCollapsed = () => { - if (this.props.isLastItem) { + if (this.props.isMostRecent) { return; } Actions.toggleMessageIdExpanded(this.props.message.id); diff --git a/app/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 b/app/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 index 87b4c27e9..886d4867a 100644 --- a/app/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 +++ b/app/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 @@ -45,7 +45,7 @@ export default class OpenTrackingComposerExtension extends ComposerExtension { // save the uid info to draft metadata metadata.uid = messageUid; - draft.applyPluginMetadata(PLUGIN_ID, metadata); + draft.directlyAttachMetadata(PLUGIN_ID, metadata); } static unapplyTransformsForSending({draftBodyRootNode}) { diff --git a/app/internal_packages/open-tracking/specs/open-tracking-composer-extension-spec.es6 b/app/internal_packages/open-tracking/specs/open-tracking-composer-extension-spec.es6 index 401687e30..96e349867 100644 --- a/app/internal_packages/open-tracking/specs/open-tracking-composer-extension-spec.es6 +++ b/app/internal_packages/open-tracking/specs/open-tracking-composer-extension-spec.es6 @@ -38,7 +38,7 @@ xdescribe('Open tracking composer extension', function openTrackingComposerExten describe("With properly formatted metadata and correct params", () => { beforeEach(() => { this.metadata = {open_count: 0}; - this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata); + this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata); OpenTrackingComposerExtension.applyTransformsForSending({ draftBodyRootNode: this.draftBodyRootNode, @@ -72,7 +72,7 @@ xdescribe('Open tracking composer extension', function openTrackingComposerExten it("removes the image from the body and restore the body to it's exact original content", () => { this.metadata = {open_count: 0}; - this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata); + this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata); this.draftBodyRootNode = nodeForHTML(afterBody); this.draft = new Message({ diff --git a/app/internal_packages/open-tracking/specs/open-tracking-icon-spec.jsx b/app/internal_packages/open-tracking/specs/open-tracking-icon-spec.jsx index 58f52c575..fc2a395c0 100644 --- a/app/internal_packages/open-tracking/specs/open-tracking-icon-spec.jsx +++ b/app/internal_packages/open-tracking/specs/open-tracking-icon-spec.jsx @@ -17,7 +17,7 @@ function find(component, className) { } function addOpenMetadata(obj, openCount) { - obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount}); + obj.directlyAttachMetadata(PLUGIN_ID, {open_count: openCount}); } describe('Open tracking icon', function openTrackingIcon() { @@ -45,7 +45,7 @@ describe('Open tracking icon', function openTrackingIcon() { }); it("shows no icon if metadata is malformed", () => { - this.messages[0].applyPluginMetadata(PLUGIN_ID, {gar: "bage"}); + this.messages[0].directlyAttachMetadata(PLUGIN_ID, {gar: "bage"}); const icon = ReactDOM.findDOMNode(makeIcon(this.thread)); expect(icon.children.length).toEqual(0); }); diff --git a/app/internal_packages/open-tracking/specs/open-tracking-message-status-spec.jsx b/app/internal_packages/open-tracking/specs/open-tracking-message-status-spec.jsx index b8186f756..a4d4ce75f 100644 --- a/app/internal_packages/open-tracking/specs/open-tracking-message-status-spec.jsx +++ b/app/internal_packages/open-tracking/specs/open-tracking-message-status-spec.jsx @@ -12,7 +12,7 @@ function makeIcon(message, props = {}) { } function addOpenMetadata(obj, openCount) { - obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount}); + obj.directlyAttachMetadata(PLUGIN_ID, {open_count: openCount}); } describe('Open tracking message status', function openTrackingMessageStatus() { @@ -28,7 +28,7 @@ describe('Open tracking message status', function openTrackingMessageStatus() { it("shows nothing if metadata is malformed", () => { - this.message.applyPluginMetadata(PLUGIN_ID, {gar: "bage"}); + this.message.directlyAttachMetadata(PLUGIN_ID, {gar: "bage"}); const icon = ReactDOM.findDOMNode(makeIcon(this.message)); expect(icon.querySelector(".open-tracking-message-status")).toBeNull(); }); diff --git a/app/spec/models/model-with-metadata-spec.es6 b/app/spec/models/model-with-metadata-spec.es6 index 065b64cf4..1297fc018 100644 --- a/app/spec/models/model-with-metadata-spec.es6 +++ b/app/spec/models/model-with-metadata-spec.es6 @@ -13,8 +13,8 @@ describe("ModelWithMetadata", function modelWithMetadata() { describe("metadataForPluginId", () => { beforeEach(() => { this.model = new TestModel(); - this.model.applyPluginMetadata('plugin-id-a', {a: true}); - this.model.applyPluginMetadata('plugin-id-b', {b: false}); + this.model.directlyAttachMetadata('plugin-id-a', {a: true}); + this.model.directlyAttachMetadata('plugin-id-b', {b: false}); }) it("returns the metadata value for the provided pluginId", () => { expect(this.model.metadataForPluginId('plugin-id-b')).toEqual({b: false}); @@ -27,21 +27,21 @@ describe("ModelWithMetadata", function modelWithMetadata() { describe("metadataObjectForPluginId", () => { it("returns the metadata object for the provided pluginId", () => { const model = new TestModel(); - model.applyPluginMetadata('plugin-id-a', {a: true}); - model.applyPluginMetadata('plugin-id-b', {b: false}); + model.directlyAttachMetadata('plugin-id-a', {a: true}); + model.directlyAttachMetadata('plugin-id-b', {b: false}); expect(model.metadataObjectForPluginId('plugin-id-a')).toEqual(model.pluginMetadata[0]); expect(model.metadataObjectForPluginId('plugin-id-b')).toEqual(model.pluginMetadata[1]); expect(model.metadataObjectForPluginId('plugin-id-c')).toEqual(undefined); }); }); - describe("applyPluginMetadata", () => { + describe("directlyAttachMetadata", () => { it("creates or updates the appropriate metadata object", () => { const model = new TestModel(); expect(model.pluginMetadata.length).toEqual(0); // create new metadata object with correct value - model.applyPluginMetadata('plugin-id-a', {a: true}); + model.directlyAttachMetadata('plugin-id-a', {a: true}); const obj = model.metadataObjectForPluginId('plugin-id-a'); expect(model.pluginMetadata.length).toEqual(1); expect(obj.pluginId).toBe('plugin-id-a'); @@ -50,28 +50,8 @@ describe("ModelWithMetadata", function modelWithMetadata() { expect(obj.value.a).toBe(true); // update existing metadata object - model.applyPluginMetadata('plugin-id-a', {a: false}); + model.directlyAttachMetadata('plugin-id-a', {a: false}); expect(obj.value.a).toBe(false); }); }); - - describe("clonePluginMetadataFrom", () => { - it(`applies the pluginMetadata from the other model, copying values \ -but resetting versions`, () => { - const model = new TestModel(); - model.applyPluginMetadata('plugin-id-a', {a: true}); - model.applyPluginMetadata('plugin-id-b', {b: false}); - model.metadataObjectForPluginId('plugin-id-a').version = 2; - model.metadataObjectForPluginId('plugin-id-b').version = 3; - - const created = new TestModel(); - created.clonePluginMetadataFrom(model); - const aMetadatum = created.metadataObjectForPluginId('plugin-id-a'); - const bMetadatum = created.metadataObjectForPluginId('plugin-id-b'); - expect(aMetadatum.version).toEqual(0); - expect(aMetadatum.value).toEqual({a: true}); - expect(bMetadatum.version).toEqual(0); - expect(bMetadatum.value).toEqual({b: false}); - }); - }); }); diff --git a/app/spec_disabled/tasks/send-draft-task-spec.es6 b/app/spec_disabled/tasks/send-draft-task-spec.es6 index 872108712..5412d1800 100644 --- a/app/spec_disabled/tasks/send-draft-task-spec.es6 +++ b/app/spec_disabled/tasks/send-draft-task-spec.es6 @@ -406,8 +406,8 @@ xdescribe('SendDraftTask', function sendDraftTask() { files: [], }); - this.draft.applyPluginMetadata('pluginIdA', {tracked: true}); - this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2}); + this.draft.directlyAttachMetadata('pluginIdA', {tracked: true}); + this.draft.directlyAttachMetadata('pluginIdB', {a: true, b: 2}); this.draft.metadataObjectForPluginId('pluginIdA').version = 2; this.task = new SendDraftTask('client-id'); @@ -446,8 +446,8 @@ xdescribe('SendDraftTask', function sendDraftTask() { files: [], }); - this.draft.applyPluginMetadata('pluginIdA', {tracked: true}); - this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2}); + this.draft.directlyAttachMetadata('pluginIdA', {tracked: true}); + this.draft.directlyAttachMetadata('pluginIdB', {a: true, b: 2}); this.draft.metadataObjectForPluginId('pluginIdA').version = 2; this.task = new SendDraftTask('client-id'); @@ -477,8 +477,8 @@ xdescribe('SendDraftTask', function sendDraftTask() { })], files: [], }); - this.task.draft.applyPluginMetadata('open-tracking', true); - this.task.draft.applyPluginMetadata('link-tracking', true); + this.task.draft.directlyAttachMetadata('open-tracking', true); + this.task.draft.directlyAttachMetadata('link-tracking', true); this.applySpies = (customValues = {}) => { let value = {provider: customValues["AccountStore.accountForId"] || "gmail"} @@ -515,20 +515,20 @@ xdescribe('SendDraftTask', function sendDraftTask() { it("should return false if neither open-tracking nor link-tracking is on", () => { this.applySpies(); - this.task.draft.applyPluginMetadata('open-tracking', false); - this.task.draft.applyPluginMetadata('link-tracking', false); + this.task.draft.directlyAttachMetadata('open-tracking', false); + this.task.draft.directlyAttachMetadata('link-tracking', false); expect(this.task.hasCustomBodyPerRecipient()).toBe(false); }); it("should return true if only open-tracking is on", () => { this.applySpies(); - this.task.draft.applyPluginMetadata('link-tracking', false); + this.task.draft.directlyAttachMetadata('link-tracking', false); expect(this.task.hasCustomBodyPerRecipient()).toBe(true); }); it("should return true if only link-tracking is on", () => { this.applySpies(); - this.task.draft.applyPluginMetadata('open-tracking', false); + this.task.draft.directlyAttachMetadata('open-tracking', false); expect(this.task.hasCustomBodyPerRecipient()).toBe(true); }); diff --git a/app/src/flux/models/model-with-metadata.es6 b/app/src/flux/models/model-with-metadata.es6 index 778c6af31..2e5992a96 100644 --- a/app/src/flux/models/model-with-metadata.es6 +++ b/app/src/flux/models/model-with-metadata.es6 @@ -5,7 +5,7 @@ import Attributes from '../attributes' Cloud-persisted data that is associated with a single Nylas API object (like a `Thread`, `Message`, or `Account`). */ -class PluginMetadata extends Model { +export class PluginMetadata extends Model { static attributes = { pluginId: Attributes.String({ modelKey: 'pluginId', @@ -71,11 +71,31 @@ export default class ModelWithMetadata extends Model { if (!metadata) { return null; } - const m = JSON.parse(JSON.stringify(metadata.value)); - if (m.expiration) { - m.expiration = new Date(m.expiration * 1000); + const value = JSON.parse(JSON.stringify(metadata.value)); + if (value.expiration) { + value.expiration = new Date(value.expiration * 1000); } - return m; + return value; + } + + /** + * Normally metadata is modified by queueing a SyncbackMetadataTask. We want changes to + * metadata to be undoable just like other draft changes in the composer. To enable this, + * we change the draft's metadata directly with other attributes and then use SyncbackDraftTask + * to commit all the changes at once. It's a bit messy: this code must match the C++ codebase. + */ + directlyAttachMetadata(pluginId, metadataValue) { + let metadata = this.metadataObjectForPluginId(pluginId); + if (!metadata) { + metadata = new PluginMetadata({pluginId, version: 0}); + this.pluginMetadata.push(metadata); + } + metadata.value = Object.assign({}, metadataValue); + metadata.version += 1; + if (metadata.value.expiration) { + metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000); + } + return this; } // Private helpers @@ -86,24 +106,4 @@ export default class ModelWithMetadata extends Model { } return this.pluginMetadata.find(metadata => metadata.pluginId === pluginId); } - - applyPluginMetadata(pluginId, metadataValue) { - let metadata = this.metadataObjectForPluginId(pluginId); - if (!metadata) { - metadata = new PluginMetadata({pluginId}); - this.pluginMetadata.push(metadata); - } - metadata.value = Object.assign({}, metadataValue); - if (metadata.value.expiration) { - metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000); - } - return this; - } - - clonePluginMetadataFrom(otherModel) { - this.pluginMetadata = otherModel.pluginMetadata.map(({pluginId, value}) => { - return new PluginMetadata({pluginId, value}); - }) - return this; - } } diff --git a/app/src/flux/stores/draft-editing-session.es6 b/app/src/flux/stores/draft-editing-session.es6 index 457f5a376..fde0fad85 100644 --- a/app/src/flux/stores/draft-editing-session.es6 +++ b/app/src/flux/stores/draft-editing-session.es6 @@ -4,6 +4,7 @@ import NylasStore from 'nylas-store'; import TaskQueue from './task-queue'; import Message from '../models/message' +import {PluginMetadata} from '../models/model-with-metadata'; import Actions from '../actions' import AccountStore from './account-store' import ContactStore from './contact-store' @@ -91,14 +92,15 @@ class DraftChangeSet extends EventEmitter { } applyToModel(model) { - if (model) { - const changesToApply = Object.entries(this._saving).concat(Object.entries(this._pending)); - for (const [key, val] of changesToApply) { - if (key.startsWith(MetadataChangePrefix)) { - model.applyPluginMetadata(key.split(MetadataChangePrefix).pop(), val); - } else { - model[key] = val; - } + if (!model) { + return null; + } + const changesToApply = Object.entries(this._saving).concat(Object.entries(this._pending)); + for (const [key, val] of changesToApply) { + if (key.startsWith(MetadataChangePrefix)) { + model.directlyAttachMetadata(key.split(MetadataChangePrefix).pop(), val); + } else { + model[key] = val; } } return model; diff --git a/app/src/flux/stores/draft-store.es6 b/app/src/flux/stores/draft-store.es6 index bf3b07200..c987e065d 100644 --- a/app/src/flux/stores/draft-store.es6 +++ b/app/src/flux/stores/draft-store.es6 @@ -324,7 +324,7 @@ class DraftStore extends NylasStore { this._doneWithSession(session); } - // Stop any pending tasks related ot the draft + // Stop any pending tasks related to the draft TaskQueue.queue().forEach((task) => { if (task instanceof SyncbackDraftTask && task.headerMessageId === headerMessageId) { Actions.cancelTask(task);