diff --git a/internal_packages/composer-emojis/package.json b/internal_packages/composer-emojis/package.json index 32ba060a8..5aa39a518 100644 --- a/internal_packages/composer-emojis/package.json +++ b/internal_packages/composer-emojis/package.json @@ -23,5 +23,5 @@ "default": true, "composer": true }, - "license": "MIT" + "license": "GPL-3.0" } diff --git a/internal_packages/github-contact-card/package.json b/internal_packages/github-contact-card/package.json index f75ecc2c1..2b7ace5a3 100644 --- a/internal_packages/github-contact-card/package.json +++ b/internal_packages/github-contact-card/package.json @@ -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" }, diff --git a/internal_packages/link-tracking/README.md b/internal_packages/link-tracking/README.md new file mode 100644 index 000000000..97f2d061f --- /dev/null +++ b/internal_packages/link-tracking/README.md @@ -0,0 +1,4 @@ + +## Open Tracking + +Adds tracking pixels to messages and tracks whether they have been opened. diff --git a/internal_packages/link-tracking/assets/linktracking-icon@2x.png b/internal_packages/link-tracking/assets/linktracking-icon@2x.png new file mode 100644 index 000000000..f03e0172e Binary files /dev/null and b/internal_packages/link-tracking/assets/linktracking-icon@2x.png differ diff --git a/internal_packages/link-tracking/lib/link-tracking-button.jsx b/internal_packages/link-tracking/lib/link-tracking-button.jsx new file mode 100644 index 000000000..282328338 --- /dev/null +++ b/internal_packages/link-tracking/lib/link-tracking-button.jsx @@ -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 ( + + + + ) + } +} + diff --git a/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 new file mode 100644 index 000000000..74adb57af --- /dev/null +++ b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 @@ -0,0 +1,46 @@ +import {ComposerExtension, Actions, QuotedHTMLTransformer} from 'nylas-exports'; +import plugin from '../package.json' + +import uuid from 'node-uuid'; + +const LINK_REGEX = (/(]*>)|(]*>)/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 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); + } + } +} diff --git a/internal_packages/link-tracking/lib/link-tracking-icon.jsx b/internal_packages/link-tracking/lib/link-tracking-icon.jsx new file mode 100644 index 000000000..962fda3cb --- /dev/null +++ b/internal_packages/link-tracking/lib/link-tracking-icon.jsx @@ -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 ( + 0 ? "clicked" : ""} + url="nylas://link-tracking/assets/linktracking-icon@2x.png" + mode={RetinaImg.Mode.ContentIsMask} /> + {clicks > 0 ? clicks : ""} + ) + } + + render() { + return ( + {this._renderIcon()} + ) + } +} diff --git a/internal_packages/link-tracking/lib/link-tracking-panel.jsx b/internal_packages/link-tracking/lib/link-tracking-panel.jsx new file mode 100644 index 000000000..26861b54b --- /dev/null +++ b/internal_packages/link-tracking/lib/link-tracking-panel.jsx @@ -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 ( + {link.url} + {link.click_count + " clicks"} + ) + }) + } + + render() { + if (this.state.links) { + return ( + Link Tracking Enabled + + + {this._renderContents()} + + + ); + } + return ; + } +} diff --git a/internal_packages/link-tracking/lib/main.es6 b/internal_packages/link-tracking/lib/main.es6 new file mode 100644 index 000000000..7dfc532e3 --- /dev/null +++ b/internal_packages/link-tracking/lib/main.es6 @@ -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() +} diff --git a/internal_packages/link-tracking/package.json b/internal_packages/link-tracking/package.json new file mode 100644 index 000000000..9e57dbda6 --- /dev/null +++ b/internal_packages/link-tracking/package.json @@ -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" +} \ No newline at end of file diff --git a/internal_packages/link-tracking/stylesheets/main.less b/internal_packages/link-tracking/stylesheets/main.less new file mode 100644 index 000000000..7f737d023 --- /dev/null +++ b/internal_packages/link-tracking/stylesheets/main.less @@ -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; +} \ No newline at end of file diff --git a/internal_packages/open-tracking/README.md b/internal_packages/open-tracking/README.md new file mode 100644 index 000000000..97f2d061f --- /dev/null +++ b/internal_packages/open-tracking/README.md @@ -0,0 +1,4 @@ + +## Open Tracking + +Adds tracking pixels to messages and tracks whether they have been opened. diff --git a/internal_packages/open-tracking/assets/envelope-closed-icon@2x.png b/internal_packages/open-tracking/assets/envelope-closed-icon@2x.png new file mode 100644 index 000000000..7014411d9 Binary files /dev/null and b/internal_packages/open-tracking/assets/envelope-closed-icon@2x.png differ diff --git a/internal_packages/open-tracking/assets/envelope-open-icon@2x.png b/internal_packages/open-tracking/assets/envelope-open-icon@2x.png new file mode 100644 index 000000000..957897e37 Binary files /dev/null and b/internal_packages/open-tracking/assets/envelope-open-icon@2x.png differ diff --git a/internal_packages/open-tracking/assets/eye@2x.png b/internal_packages/open-tracking/assets/eye@2x.png new file mode 100644 index 000000000..0766e833f Binary files /dev/null and b/internal_packages/open-tracking/assets/eye@2x.png differ diff --git a/internal_packages/open-tracking/lib/main.es6 b/internal_packages/open-tracking/lib/main.es6 new file mode 100644 index 000000000..592814cf9 --- /dev/null +++ b/internal_packages/open-tracking/lib/main.es6 @@ -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() +} diff --git a/internal_packages/open-tracking/lib/open-tracking-button.jsx b/internal_packages/open-tracking/lib/open-tracking-button.jsx new file mode 100644 index 000000000..5e01c3285 --- /dev/null +++ b/internal_packages/open-tracking/lib/open-tracking-button.jsx @@ -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 ( + + ) + } + +} diff --git a/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 b/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 new file mode 100644 index 000000000..f77ff88fb --- /dev/null +++ b/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6 @@ -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 into the message + const serverUrl = `http://${PLUGIN_URL}/${draft.accountId}/${uid}`; + const img = ``; + const draftBody = new DraftBody(draft); + draftBody.unquoted = draftBody.unquoted + "" + 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); + } + } +} diff --git a/internal_packages/open-tracking/lib/open-tracking-icon.jsx b/internal_packages/open-tracking/lib/open-tracking-icon.jsx new file mode 100644 index 000000000..972bdde4f --- /dev/null +++ b/internal_packages/open-tracking/lib/open-tracking-icon.jsx @@ -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 ; + } else if (this.state.opened) { + return ( + + ); + } + return ( + + ); + }; + + render() { + return ( + + {this._renderIcon()} + + ); + } +} diff --git a/internal_packages/open-tracking/package.json b/internal_packages/open-tracking/package.json new file mode 100644 index 000000000..940bcf99d --- /dev/null +++ b/internal_packages/open-tracking/package.json @@ -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" +} \ No newline at end of file diff --git a/internal_packages/open-tracking/stylesheets/main.less b/internal_packages/open-tracking/stylesheets/main.less new file mode 100644 index 000000000..65d1401ae --- /dev/null +++ b/internal_packages/open-tracking/stylesheets/main.less @@ -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; +} \ No newline at end of file diff --git a/internal_packages/personal-level-indicators/package.json b/internal_packages/personal-level-indicators/package.json index 7c9ae9838..91a7e00d3 100644 --- a/internal_packages/personal-level-indicators/package.json +++ b/internal_packages/personal-level-indicators/package.json @@ -15,6 +15,6 @@ "engines": { "nylas": ">=0.3.0 <0.5.0" }, - "dependencies": [], - "license": "MIT" + "dependencies": {}, + "license": "GPL-3.0" } diff --git a/internal_packages/quick-schedule/package.json b/internal_packages/quick-schedule/package.json index 6813427c3..221dcb847 100644 --- a/internal_packages/quick-schedule/package.json +++ b/internal_packages/quick-schedule/package.json @@ -23,5 +23,5 @@ "default": true, "composer": true }, - "license": "MIT" + "license": "GPL-3.0" } diff --git a/src/package.coffee b/src/package.coffee index 551f5f253..939d389b1 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -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