fix(link/open tracking): Remove intermediate metadata states, extra db lookup, dead code

This commit is contained in:
Ben Gotow 2016-02-25 12:48:05 -08:00
parent 274131863f
commit fe27fb161f
12 changed files with 69 additions and 191 deletions

View file

@ -29,7 +29,7 @@ export default class LinkTrackingButton extends React.Component {
iconName="icon-composer-linktracking.png"
pluginId={PLUGIN_ID}
pluginName={PLUGIN_NAME}
metadataKey="tracked"
metadataEnabledValue={{"tracked": true}}
stickyToggle
errorMessage={this._errorMessage}
draftClientId={this.props.draftClientId} />

View file

@ -42,6 +42,7 @@ export default class LinkTrackingComposerExtension extends ComposerExtension {
// save the link info to draft metadata
metadata.uid = messageUid;
metadata.links = links;
Actions.setMetadata(draft, PLUGIN_ID, metadata);
}
}

View file

@ -1,59 +0,0 @@
import {React} from 'nylas-exports'
import {RetinaImg} from 'nylas-component-kit'
import {PLUGIN_ID} from './link-tracking-constants'
const sum = (array, extractFn) => array.reduce( (a, b) => a + extractFn(b), 0 );
export default class LinkTrackingIcon extends React.Component {
static displayName = 'LinkTrackingIcon';
static propTypes = {
thread: React.PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = this._getStateFromThread(props.thread);
}
componentWillReceiveProps(newProps) {
this.setState(this._getStateFromThread(newProps.thread));
}
_getStateFromThread(thread) {
const messages = thread.metadata;
// Pull a list of metadata for all messages
const metadataObjs = messages.map(msg => msg.metadataForPluginId(PLUGIN_ID)).filter(meta => meta);
if (metadataObjs.length) {
// If there's metadata, return the total number of link clicks in the most recent metadata
const mostRecentMetadata = metadataObjs.pop();
return {
clicks: sum(mostRecentMetadata.links || [], link => link.click_count || 0),
};
}
return {clicks: null};
}
_renderIcon = () => {
return this.state.clicks == null ? "" : this._getIcon(this.state.clicks);
};
_getIcon(clicks) {
return (<span>
<RetinaImg
className={clicks > 0 ? "clicked" : ""}
name="icon-composer-linktracking.png"
mode={RetinaImg.Mode.ContentIsMask} />
<span className="link-click-count">{clicks > 0 ? clicks : ""}</span>
</span>)
}
render() {
return (<div className="link-tracking-icon">
{this._renderIcon()}
</div>)
}
}

View file

@ -1,48 +0,0 @@
import {React} from 'nylas-exports'
import {PLUGIN_ID} from './link-tracking-constants'
export default class LinkTrackingPanel extends React.Component {
static displayName = 'LinkTrackingPanel';
static propTypes = {
message: React.PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = this._getStateFromMessage(props.message)
}
componentWillReceiveProps(newProps) {
this.setState(this._getStateFromMessage(newProps.message));
}
_getStateFromMessage(message) {
const metadata = message.metadataForPluginId(PLUGIN_ID);
return metadata ? {links: metadata.links} : {};
}
_renderContents() {
return this.state.links.map(link => {
return (<tr className="link-info">
<td className="link-url">{link.url}</td>
<td className="link-count">{link.click_count + " clicks"}</td>
</tr>)
})
}
render() {
if (this.state.links) {
return (<div className="link-tracking-panel">
<h4>Link Tracking Enabled</h4>
<table>
<tbody>
{this._renderContents()}
</tbody>
</table>
</div>);
}
return <div></div>;
}
}

View file

@ -1,9 +1,6 @@
import request from 'request';
import {ComponentRegistry, DatabaseStore, Message, ExtensionRegistry, Actions} from 'nylas-exports';
import {ComponentRegistry, ExtensionRegistry, Actions} from 'nylas-exports';
import LinkTrackingButton from './link-tracking-button';
// TODO what's up with these components?
// import LinkTrackingIcon from './link-tracking-icon';
// import LinkTrackingPanel from './link-tracking-panel';
import LinkTrackingComposerExtension from './link-tracking-composer-extension';
import LinkTrackingMessageExtension from './link-tracking-message-extension';
import {PLUGIN_ID, PLUGIN_URL} from './link-tracking-constants';
@ -11,41 +8,36 @@ import {PLUGIN_ID, PLUGIN_URL} from './link-tracking-constants';
const post = Promise.promisify(request.post, {multiArgs: true});
function afterDraftSend({draftClientId}) {
function afterDraftSend({message}) {
// only run this handler in the main window
if (!NylasEnv.isMainWindow()) return;
// query for the message
DatabaseStore.findBy(Message, {clientId: draftClientId}).then((message) => {
// grab message metadata, if any
const metadata = message.metadataForPluginId(PLUGIN_ID);
// get the uid from the metadata, if present
if (metadata) {
const uid = metadata.uid;
// grab message metadata, if any
const metadata = message.metadataForPluginId(PLUGIN_ID);
if (metadata) {
// get the uid from the metadata, if present
const uid = metadata.uid;
// post the uid and message id pair to the plugin server
const data = {uid: uid, message_id: message.id};
const serverUrl = `${PLUGIN_URL}/plugins/register-message`;
return post({
url: serverUrl,
body: JSON.stringify(data),
}).then( ([response, responseBody]) => {
if (response.statusCode !== 200) {
throw new Error(`Link Tracking server error ${response.statusCode} at ${serverUrl}: ${responseBody}`);
}
return responseBody;
}).catch(error => {
NylasEnv.showErrorDialog("There was a problem contacting the Link Tracking server! This message will not have link tracking");
Promise.reject(error);
});
}
});
// post the uid and message id pair to the plugin server
const data = {uid: uid, message_id: message.id};
const serverUrl = `${PLUGIN_URL}/plugins/register-message`;
post({
url: serverUrl,
body: JSON.stringify(data),
}).then( ([response, responseBody]) => {
if (response.statusCode !== 200) {
throw new Error(`Link Tracking server error ${response.statusCode} at ${serverUrl}: ${responseBody}`);
}
}).catch(error => {
NylasEnv.showErrorDialog("There was a problem contacting the Link Tracking server! This message will not have link tracking");
Promise.reject(error);
});
}
}
export function activate() {
ComponentRegistry.register(LinkTrackingButton, {role: 'Composer:ActionButton'});
// ComponentRegistry.register(LinkTrackingIcon, {role: 'ThreadListIcon'});
// ComponentRegistry.register(LinkTrackingPanel, {role: 'message:BodyHeader'});
ExtensionRegistry.Composer.register(LinkTrackingComposerExtension);
ExtensionRegistry.MessageView.register(LinkTrackingMessageExtension);
this._unlistenSendDraftSuccess = Actions.sendDraftSuccess.listen(afterDraftSend);
@ -55,8 +47,6 @@ export function serialize() {}
export function deactivate() {
ComponentRegistry.unregister(LinkTrackingButton);
// ComponentRegistry.unregister(LinkTrackingIcon);
// ComponentRegistry.unregister(LinkTrackingPanel);
ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension);
ExtensionRegistry.MessageView.unregister(LinkTrackingMessageExtension);
this._unlistenSendDraftSuccess()

View file

@ -1,5 +1,5 @@
import request from 'request';
import {ComponentRegistry, ExtensionRegistry, DatabaseStore, Message, Actions} from 'nylas-exports';
import {ComponentRegistry, ExtensionRegistry, Actions} from 'nylas-exports';
import OpenTrackingButton from './open-tracking-button';
import OpenTrackingIcon from './open-tracking-icon';
import OpenTrackingMessageStatus from './open-tracking-message-status';
@ -9,39 +9,33 @@ import {PLUGIN_ID, PLUGIN_URL} from './open-tracking-constants'
const post = Promise.promisify(request.post, {multiArgs: true});
function afterDraftSend({draftClientId}) {
function afterDraftSend({message}) {
// only run this handler in the main window
if (!NylasEnv.isMainWindow()) return;
// query for the message
DatabaseStore.findBy(Message, {clientId: draftClientId}).then((message) => {
// grab message metadata, if any
const metadata = message.metadataForPluginId(PLUGIN_ID);
// grab message metadata, if any
const metadata = message.metadataForPluginId(PLUGIN_ID);
// get the uid from the metadata, if present
if (metadata) {
const uid = metadata.uid;
// get the uid from the metadata, if present
if (metadata) {
const uid = metadata.uid;
// set metadata against the message
Actions.setMetadata(message, PLUGIN_ID, {open_count: 0, open_data: []});
// post the uid and message id pair to the plugin server
const data = {uid: uid, message_id: message.id, thread_id: 1};
const serverUrl = `${PLUGIN_URL}/plugins/register-message`;
// post the uid and message id pair to the plugin server
const data = {uid: uid, message_id: message.id, thread_id: 1};
const serverUrl = `${PLUGIN_URL}/plugins/register-message`;
return post({
url: serverUrl,
body: JSON.stringify(data),
}).then(([response, responseBody]) => {
if (response.statusCode !== 200) {
throw new Error();
}
return responseBody;
}).catch(error => {
NylasEnv.showErrorDialog("There was a problem contacting the Open Tracking server! This message will not have open tracking :(");
Promise.reject(error);
});
}
});
post({
url: serverUrl,
body: JSON.stringify(data),
}).then(([response, responseBody]) => {
if (response.statusCode !== 200) {
throw new Error(responseBody);
}
}).catch(error => {
NylasEnv.showErrorDialog(`There was a problem saving your open tracking settings. This message will not have open tracking. ${error.message}`);
Promise.reject(error);
});
}
}
export function activate() {

View file

@ -2,6 +2,7 @@
import {React, APIError, NylasAPI} from 'nylas-exports'
import {MetadataComposerToggleButton} from 'nylas-component-kit'
import {PLUGIN_ID, PLUGIN_NAME} from './open-tracking-constants'
import uuid from 'node-uuid';
export default class OpenTrackingButton extends React.Component {
static displayName = 'OpenTrackingButton';
@ -23,13 +24,19 @@ export default class OpenTrackingButton extends React.Component {
}
render() {
const enabledValue = {
uid: uuid.v4().replace(/-/g, ""),
open_count: 0,
open_data: [],
};
return (
<MetadataComposerToggleButton
title={this._title}
iconUrl="nylas://open-tracking/assets/icon-composer-eye@2x.png"
pluginId={PLUGIN_ID}
pluginName={PLUGIN_NAME}
metadataKey="tracked"
metadataEnabledValue={enabledValue}
stickyToggle
errorMessage={this._errorMessage}
draftClientId={this.props.draftClientId} />

View file

@ -1,5 +1,4 @@
import uuid from 'node-uuid';
import {ComposerExtension, Actions, QuotedHTMLTransformer} from 'nylas-exports';
import {ComposerExtension, QuotedHTMLTransformer} from 'nylas-exports';
import {PLUGIN_ID, PLUGIN_URL} from './open-tracking-constants';
@ -17,11 +16,8 @@ export default class OpenTrackingComposerExtension extends ComposerExtension {
// grab message metadata, if any
const metadata = draft.metadataForPluginId(PLUGIN_ID);
if (metadata) {
// generate a UID
const uid = uuid.v4().replace(/-/g, "");
// insert a tracking pixel <img> into the message
const serverUrl = `${PLUGIN_URL}/open/${draft.accountId}/${uid}`;
const serverUrl = `${PLUGIN_URL}/open/${draft.accountId}/${metadata.uid}`;
const img = `<img width="0" height="0" style="border:0; width:0; height:0;" src="${serverUrl}">`;
const draftBody = new DraftBody(draft);
draftBody.unquoted = draftBody.unquoted + "<br>" + img;
@ -29,10 +25,6 @@ export default class OpenTrackingComposerExtension extends ComposerExtension {
// save the draft
session.changes.add({body: draftBody.body});
session.changes.commit();
// save the uid to draft metadata
metadata.uid = uid;
Actions.setMetadata(draft, PLUGIN_ID, metadata);
}
}
}

View file

@ -115,7 +115,8 @@ describe "SendDraftTask", ->
waitsForPromise =>
@task.performRemote().then =>
args = Actions.sendDraftSuccess.calls[0].args[0]
expect(args.draftClientId).toBe @draft.clientId
expect(args.message instanceof Message).toBe(true)
expect(args.messageClientId).toBe(@draft.clientId)
it "should play a sound", ->
spyOn(NylasEnv.config, "get").andReturn true

View file

@ -52,6 +52,7 @@ class ExtendedSelection
selectFromTo: (from, to) ->
fromNode = @findNodeAt(from)
toNode = @findNodeAt(to)
return unless fromNode and toNode
@setBaseAndExtent(fromNode, 0, toNode, (toNode.length ? 0))
selectFromToWithIndex: (from, fromIndex, to, toIndex) ->
@ -59,6 +60,7 @@ class ExtendedSelection
toNode = @findNodeAt(to)
if (not _.isNumber(fromIndex)) or (not _.isNumber(toIndex))
throw @_errBadUsage()
return unless fromNode and toNode
@setBaseAndExtent(fromNode, fromIndex, toNode, toIndex)
exportSelection: -> new ExportedSelection(@rawSelection, @scopeNode)

View file

@ -12,7 +12,7 @@ export default class MetadataComposerToggleButton extends React.Component {
iconName: React.PropTypes.string,
pluginId: React.PropTypes.string.isRequired,
pluginName: React.PropTypes.string.isRequired,
metadataKey: React.PropTypes.string.isRequired,
metadataEnabledValue: React.PropTypes.object.isRequired,
stickyToggle: React.PropTypes.bool,
errorMessage: React.PropTypes.func.isRequired,
draftClientId: React.PropTypes.string.isRequired,
@ -55,21 +55,19 @@ export default class MetadataComposerToggleButton extends React.Component {
if (!metadata) {
if (!this.state.isSetup) {
if (this._isDefaultOn()) {
this._setMetadataValueTo(true)
this._setEnabled(true)
}
this.setState({isSetup: true})
}
} else {
this.setState({enabled: metadata.tracked, isSetup: true});
this.setState({enabled: true, isSetup: true});
}
};
_setMetadataValueTo(enabled) {
const newValue = {}
newValue[this.props.metadataKey] = enabled
_setEnabled(enabled) {
const metadataValue = enabled ? this.props.metadataEnabledValue : null;
this.setState({enabled, pending: true});
const metadataValue = enabled ? newValue : null
// write metadata into the draft to indicate tracked state
return DraftStore.sessionForClientId(this.props.draftClientId).then((session)=> {
const draft = session.draft();
@ -108,7 +106,7 @@ export default class MetadataComposerToggleButton extends React.Component {
if (this.props.stickyToggle) {
NylasEnv.config.set(this._configKey(), !this.state.enabled)
}
this._setMetadataValueTo(!this.state.enabled)
this._setEnabled(!this.state.enabled)
};
render() {

View file

@ -199,7 +199,7 @@ class SendDraftTask extends Task
Actions.queueTask(task)
)
Actions.sendDraftSuccess draftClientId: @message.clientId
Actions.sendDraftSuccess(message: @message, messageClientId: @message.clientId)
# Play the sending sound
if NylasEnv.config.get("core.sending.sounds")