mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
feat(new-plugins): add open tracking and link tracking plugins
Summary: Adds two (very similar) plugins - Open Tracking and Link Tracking. Both can be enabled via a button in the composer. Open tracking inserts a tracking pixel right before send, and link tracking replaces all links with tracked redirects. Both plugins use the new Metadata service to store their open/click counts, and have backend servers to respectively serve the pixel image or handle the redirects. Requests also trigger a metadata update to increment the open/click counters. Test Plan: Manual for now Reviewers: evan, bengotow, drew Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2583
This commit is contained in:
parent
8a42cefd40
commit
fe7a894e51
24 changed files with 617 additions and 6 deletions
|
@ -23,5 +23,5 @@
|
|||
"default": true,
|
||||
"composer": true
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"description": "Extends the contact card in the sidebar to show public repos of the people you email.",
|
||||
"icon": "./icon.png",
|
||||
|
||||
"license": "MIT",
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"nylas": ">=0.3.0 <0.5.0"
|
||||
},
|
||||
|
|
4
internal_packages/link-tracking/README.md
Normal file
4
internal_packages/link-tracking/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
## Open Tracking
|
||||
|
||||
Adds tracking pixels to messages and tracks whether they have been opened.
|
BIN
internal_packages/link-tracking/assets/linktracking-icon@2x.png
Normal file
BIN
internal_packages/link-tracking/assets/linktracking-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 635 B |
59
internal_packages/link-tracking/lib/link-tracking-button.jsx
Normal file
59
internal_packages/link-tracking/lib/link-tracking-button.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import plugin from '../package.json'
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
|
||||
export default class LinkTrackingButton extends React.Component {
|
||||
static displayName = 'LinkTrackingButton';
|
||||
|
||||
static propTypes = {
|
||||
draftClientId: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {enabled: false};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const query = DatabaseStore.findBy(Message, {clientId: this.props.draftClientId});
|
||||
this._subscription = Rx.Observable.fromQuery(query).subscribe(this.setStateFromDraft)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._subscription.dispose();
|
||||
}
|
||||
|
||||
setStateFromDraft =(draft)=> {
|
||||
if (!draft) return;
|
||||
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
||||
this.setState({enabled: metadata ? metadata.tracked : false});
|
||||
};
|
||||
|
||||
_onClick=()=> {
|
||||
const currentlyEnabled = this.state.enabled;
|
||||
|
||||
// write metadata into the draft to indicate tracked state
|
||||
DraftStore.sessionForClientId(this.props.draftClientId)
|
||||
.then(session => session.draft())
|
||||
.then(draft => {
|
||||
return NylasAPI.authPlugin(PLUGIN_ID, plugin.title, draft.accountId).then(() => {
|
||||
Actions.setMetadata(draft, PLUGIN_ID, currentlyEnabled ? null : {tracked: true});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
title="Link Tracking"
|
||||
className={`btn btn-toolbar ${this.state.enabled ? "btn-action" : ""}`}
|
||||
onClick={this._onClick}>
|
||||
<RetinaImg
|
||||
url="nylas://link-tracking/assets/linktracking-icon@2x.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import {ComposerExtension, Actions, QuotedHTMLTransformer} from 'nylas-exports';
|
||||
import plugin from '../package.json'
|
||||
|
||||
import uuid from 'node-uuid';
|
||||
|
||||
const LINK_REGEX = (/(<a\s.*?href\s*?=\s*?")([^"]*)("[^>]*>)|(<a\s.*?href\s*?=\s*?')([^']*)('[^>]*>)/g);
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
const PLUGIN_URL = "n1-link-tracking.herokuapp.com";
|
||||
|
||||
class DraftBody {
|
||||
constructor(draft) {this._body = draft.body}
|
||||
get unquoted() {return QuotedHTMLTransformer.removeQuotedHTML(this._body);}
|
||||
set unquoted(text) {this._body = QuotedHTMLTransformer.appendQuotedHTML(text, this._body);}
|
||||
get body() {return this._body}
|
||||
}
|
||||
|
||||
export default class LinkTrackingComposerExtension extends ComposerExtension {
|
||||
static finalizeSessionBeforeSending({session}) {
|
||||
const draft = session.draft();
|
||||
|
||||
// grab message metadata, if any
|
||||
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
||||
if (metadata) {
|
||||
const draftBody = new DraftBody(draft);
|
||||
const links = [];
|
||||
const messageUid = uuid.v4().replace(/-/g, "");
|
||||
|
||||
// loop through all <a href> elements, replace with redirect links and save mappings
|
||||
draftBody.unquoted = draftBody.unquoted.replace(LINK_REGEX, (match, prefix, url, suffix) => {
|
||||
const encoded = encodeURIComponent(url);
|
||||
const redirectUrl = `http://${PLUGIN_URL}/${draft.accountId}/${messageUid}/${links.length}?redirect=${encoded}`;
|
||||
links.push({url: url, click_count: 0, click_data: []});
|
||||
return prefix + redirectUrl + suffix;
|
||||
});
|
||||
|
||||
// save the draft
|
||||
session.changes.add({body: draftBody.body});
|
||||
session.changes.commit();
|
||||
|
||||
// save the link info to draft metadata
|
||||
metadata.uid = messageUid;
|
||||
metadata.links = links;
|
||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
||||
}
|
||||
}
|
||||
}
|
58
internal_packages/link-tracking/lib/link-tracking-icon.jsx
Normal file
58
internal_packages/link-tracking/lib/link-tracking-icon.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import {React} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import plugin from '../package.json'
|
||||
|
||||
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.appId)).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" : ""}
|
||||
url="nylas://link-tracking/assets/linktracking-icon@2x.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>)
|
||||
}
|
||||
}
|
47
internal_packages/link-tracking/lib/link-tracking-panel.jsx
Normal file
47
internal_packages/link-tracking/lib/link-tracking-panel.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {React} from 'nylas-exports'
|
||||
import plugin from '../package.json'
|
||||
|
||||
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.appId);
|
||||
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>;
|
||||
}
|
||||
}
|
61
internal_packages/link-tracking/lib/main.es6
Normal file
61
internal_packages/link-tracking/lib/main.es6
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {ComponentRegistry, DatabaseStore, Message, ExtensionRegistry, Actions} from 'nylas-exports';
|
||||
import LinkTrackingButton from './link-tracking-button';
|
||||
import LinkTrackingIcon from './link-tracking-icon';
|
||||
import LinkTrackingComposerExtension from './link-tracking-composer-extension';
|
||||
import LinkTrackingPanel from './link-tracking-panel';
|
||||
import plugin from '../package.json'
|
||||
|
||||
import request from 'request';
|
||||
|
||||
const post = Promise.promisify(request.post, {multiArgs: true});
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
const PLUGIN_URL = "n1-link-tracking.herokuapp.com";
|
||||
|
||||
function afterDraftSend({draftClientId}) {
|
||||
// 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;
|
||||
|
||||
// post the uid and message id pair to the plugin server
|
||||
const data = {uid: uid, message_id: message.id};
|
||||
const serverUrl = `http://${PLUGIN_URL}/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 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);
|
||||
this._unlistenSendDraftSuccess = Actions.sendDraftSuccess.listen(afterDraftSend);
|
||||
}
|
||||
|
||||
export function serialize() {}
|
||||
|
||||
export function deactivate() {
|
||||
ComponentRegistry.unregister(LinkTrackingButton);
|
||||
ComponentRegistry.unregister(LinkTrackingIcon);
|
||||
ComponentRegistry.unregister(LinkTrackingPanel);
|
||||
ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension);
|
||||
this._unlistenSendDraftSuccess()
|
||||
}
|
25
internal_packages/link-tracking/package.json
Normal file
25
internal_packages/link-tracking/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "link-tracking",
|
||||
"main": "./lib/main",
|
||||
"version": "0.1.0",
|
||||
"appId":"ybqmsi39th16ka60dcxz1in9",
|
||||
|
||||
"title": "Link Tracking",
|
||||
"description": "Tracks whether links in an email have been clicked by recipients",
|
||||
"icon": "./icon.png",
|
||||
"isOptional": true,
|
||||
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"engines": {
|
||||
"nylas": ">=0.3.46"
|
||||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
"composer": true
|
||||
},
|
||||
"dependencies": {},
|
||||
"license": "GPL-3.0"
|
||||
}
|
47
internal_packages/link-tracking/stylesheets/main.less
Normal file
47
internal_packages/link-tracking/stylesheets/main.less
Normal file
|
@ -0,0 +1,47 @@
|
|||
@import "ui-variables";
|
||||
@import "ui-mixins";
|
||||
|
||||
|
||||
.link-tracking-icon img.content-mask {
|
||||
background-color: #AAA;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.link-tracking-icon img.content-mask.clicked {
|
||||
background-color: #CCC;
|
||||
}
|
||||
.link-tracking-icon .link-click-count {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -16px;
|
||||
text-align: center;
|
||||
|
||||
color: #3187e1;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.link-tracking-icon {
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
|
||||
.link-tracking-panel {
|
||||
background: #DDF6FF;
|
||||
border: 1px solid #ACD;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.link-tracking-panel h4{
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
.link-tracking-panel table{
|
||||
width: 100%;
|
||||
}
|
||||
.link-tracking-panel td {
|
||||
border-bottom: 1px solid #D5EAF5;
|
||||
border-top: 1px solid #D5EAF5;
|
||||
padding: 0 10px;
|
||||
text-align: left;
|
||||
}
|
4
internal_packages/open-tracking/README.md
Normal file
4
internal_packages/open-tracking/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
## Open Tracking
|
||||
|
||||
Adds tracking pixels to messages and tracks whether they have been opened.
|
Binary file not shown.
After Width: | Height: | Size: 519 B |
BIN
internal_packages/open-tracking/assets/envelope-open-icon@2x.png
Normal file
BIN
internal_packages/open-tracking/assets/envelope-open-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 638 B |
BIN
internal_packages/open-tracking/assets/eye@2x.png
Normal file
BIN
internal_packages/open-tracking/assets/eye@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
62
internal_packages/open-tracking/lib/main.es6
Normal file
62
internal_packages/open-tracking/lib/main.es6
Normal file
|
@ -0,0 +1,62 @@
|
|||
import {ComponentRegistry, ExtensionRegistry, DatabaseStore, Message, Actions} from 'nylas-exports';
|
||||
import OpenTrackingButton from './open-tracking-button';
|
||||
import OpenTrackingIcon from './open-tracking-icon';
|
||||
import OpenTrackingComposerExtension from './open-tracking-composer-extension';
|
||||
import plugin from '../package.json'
|
||||
|
||||
import request from 'request';
|
||||
|
||||
const post = Promise.promisify(request.post, {multiArgs: true});
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
const PLUGIN_URL = "n1-open-tracking.herokuapp.com";
|
||||
|
||||
function afterDraftSend({draftClientId}) {
|
||||
// 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;
|
||||
|
||||
// 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 = `http://${PLUGIN_URL}/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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function activate() {
|
||||
ComponentRegistry.register(OpenTrackingButton, {role: 'Composer:ActionButton'});
|
||||
ComponentRegistry.register(OpenTrackingIcon, {role: 'ThreadListIcon'});
|
||||
ExtensionRegistry.Composer.register(OpenTrackingComposerExtension);
|
||||
this._unlistenSendDraftSuccess = Actions.sendDraftSuccess.listen(afterDraftSend);
|
||||
}
|
||||
|
||||
export function serialize() {}
|
||||
|
||||
export function deactivate() {
|
||||
ComponentRegistry.unregister(OpenTrackingButton);
|
||||
ComponentRegistry.unregister(OpenTrackingIcon);
|
||||
ExtensionRegistry.Composer.unregister(OpenTrackingComposerExtension);
|
||||
this._unlistenSendDraftSuccess()
|
||||
}
|
55
internal_packages/open-tracking/lib/open-tracking-button.jsx
Normal file
55
internal_packages/open-tracking/lib/open-tracking-button.jsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import plugin from '../package.json'
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
|
||||
export default class OpenTrackingButton extends React.Component {
|
||||
|
||||
static displayName = 'OpenTrackingButton';
|
||||
|
||||
static propTypes = {
|
||||
draftClientId: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {enabled: false};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const query = DatabaseStore.findBy(Message, {clientId: this.props.draftClientId});
|
||||
this._subscription = Rx.Observable.fromQuery(query).subscribe(this.setStateFromDraft)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._subscription.dispose();
|
||||
}
|
||||
|
||||
setStateFromDraft = (draft)=> {
|
||||
if (!draft) return;
|
||||
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
||||
this.setState({enabled: metadata ? metadata.tracked : false});
|
||||
};
|
||||
|
||||
_onClick=()=> {
|
||||
const currentlyEnabled = this.state.enabled;
|
||||
|
||||
// write metadata into the draft to indicate tracked state
|
||||
DraftStore.sessionForClientId(this.props.draftClientId)
|
||||
.then(session => session.draft())
|
||||
.then(draft => {
|
||||
return NylasAPI.authPlugin(PLUGIN_ID, plugin.title, draft.accountId).then(() => {
|
||||
Actions.setMetadata(draft, PLUGIN_ID, currentlyEnabled ? null : {tracked: true});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (<button className={`btn btn-toolbar ${this.state.enabled ? "btn-action" : ""}`}
|
||||
onClick={this._onClick} title="Open Tracking">
|
||||
<RetinaImg url="nylas://open-tracking/assets/envelope-open-icon@2x.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import {ComposerExtension, Actions, QuotedHTMLTransformer} from 'nylas-exports';
|
||||
import plugin from '../package.json'
|
||||
|
||||
import uuid from 'node-uuid';
|
||||
|
||||
const PLUGIN_ID = plugin.appId;
|
||||
const PLUGIN_URL = "n1-open-tracking.herokuapp.com";
|
||||
|
||||
class DraftBody {
|
||||
constructor(draft) {this._body = draft.body}
|
||||
get unquoted() {return QuotedHTMLTransformer.removeQuotedHTML(this._body);}
|
||||
set unquoted(text) {this._body = QuotedHTMLTransformer.appendQuotedHTML(text, this._body);}
|
||||
get body() {return this._body}
|
||||
}
|
||||
|
||||
export default class OpenTrackingComposerExtension extends ComposerExtension {
|
||||
static finalizeSessionBeforeSending({session}) {
|
||||
const draft = session.draft();
|
||||
|
||||
// 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 = `http://${PLUGIN_URL}/${draft.accountId}/${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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
52
internal_packages/open-tracking/lib/open-tracking-icon.jsx
Normal file
52
internal_packages/open-tracking/lib/open-tracking-icon.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {React} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import plugin from '../package.json'
|
||||
|
||||
export default class OpenTrackingIcon extends React.Component {
|
||||
static displayName = 'OpenTrackingIcon';
|
||||
|
||||
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;
|
||||
const metadataObjs = messages.map(msg => msg.metadataForPluginId(plugin.appId)).filter(meta => meta);
|
||||
return {opened: metadataObjs.length ? metadataObjs.every(m => m.open_count > 0) : null};
|
||||
}
|
||||
|
||||
_renderIcon = () => {
|
||||
if (this.state.opened == null) {
|
||||
return <span />;
|
||||
} else if (this.state.opened) {
|
||||
return (
|
||||
<RetinaImg
|
||||
url="nylas://open-tracking/assets/envelope-open-icon@2x.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RetinaImg
|
||||
className="unopened"
|
||||
url="nylas://open-tracking/assets/envelope-closed-icon@2x.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="open-tracking-icon">
|
||||
{this._renderIcon()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
25
internal_packages/open-tracking/package.json
Normal file
25
internal_packages/open-tracking/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "open-tracking",
|
||||
"main": "./lib/main",
|
||||
"version": "0.1.0",
|
||||
"appId":"9tm0s8yzzkaahi5nql34iw8wu",
|
||||
|
||||
"title": "Open Tracking",
|
||||
"description": "Tracks whether email messages have been opened by recipients",
|
||||
"icon": "./icon.png",
|
||||
"isOptional": true,
|
||||
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"engines": {
|
||||
"nylas": ">=0.3.46"
|
||||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
"composer": true
|
||||
},
|
||||
"dependencies": {},
|
||||
"license": "GPL-3.0"
|
||||
}
|
24
internal_packages/open-tracking/stylesheets/main.less
Normal file
24
internal_packages/open-tracking/stylesheets/main.less
Normal file
|
@ -0,0 +1,24 @@
|
|||
@import "ui-variables";
|
||||
@import "ui-mixins";
|
||||
|
||||
.open-tracking-icon img.content-mask {
|
||||
background-color: #AAA;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.open-tracking-icon img.content-mask.unopened {
|
||||
background-color: #C00;
|
||||
}
|
||||
.open-tracking-icon .open-count {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: -16px;
|
||||
text-align: center;
|
||||
|
||||
color: #3187e1;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.open-tracking-icon {
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
|
@ -15,6 +15,6 @@
|
|||
"engines": {
|
||||
"nylas": ">=0.3.0 <0.5.0"
|
||||
},
|
||||
"dependencies": [],
|
||||
"license": "MIT"
|
||||
"dependencies": {},
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
|
|
|
@ -23,5 +23,5 @@
|
|||
"default": true,
|
||||
"composer": true
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ class Package
|
|||
@metadata ?= Package.loadMetadata(@path)
|
||||
@bundledPackage = Package.isBundledPackagePath(@path)
|
||||
@name = @metadata?.name ? path.basename(@path)
|
||||
@pluginAppId = @metadata.appId ? null
|
||||
@displayName = @metadata?.displayName || @name
|
||||
ModuleCache.add(@path, @metadata)
|
||||
@reset()
|
||||
|
@ -82,7 +83,7 @@ class Package
|
|||
|
||||
# TODO FIXME: Use a unique pluginID instead of just the "name"
|
||||
# This needs to be included here to prevent a circular dependency error
|
||||
pluginId: -> return @name
|
||||
pluginId: -> return @pluginAppId ? @name
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
|
|
Loading…
Reference in a new issue