mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
Correctly increment metadata version when modifying drafts
This commit is contained in:
parent
a6641dc7da
commit
7b6f8ca81a
|
@ -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() {
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue