From 6e48af0c90aea3b24862c7944c9d78ce1024fe5e Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Tue, 23 Feb 2016 18:20:26 -0800 Subject: [PATCH] fix(read-receipts): Style fixes to link tracking and read receipts --- .../assets/ic-tracking-unvisited@1x.png | Bin 0 -> 241 bytes .../assets/ic-tracking-unvisited@2x.png | Bin 0 -> 381 bytes .../assets/ic-tracking-visited@1x.png | Bin 0 -> 233 bytes .../assets/ic-tracking-visited@2x.png | Bin 0 -> 358 bytes .../lib/link-tracking-composer-extension.es6 | 13 +++-- .../link-tracking/lib/link-tracking-icon.jsx | 2 +- .../lib/link-tracking-message-extension.es6 | 49 ++++++++++++++++++ .../link-tracking/lib/link-tracking-panel.jsx | 4 +- internal_packages/link-tracking/lib/main.es6 | 9 ++-- .../message-list/lib/message-item.cjsx | 8 +-- .../lib/plugins/autolinker-extension.coffee | 21 ++++++-- .../spec/autolinker-extension-spec.coffee | 4 +- .../onboarding/lib/token-auth-api.coffee | 3 ++ internal_packages/open-tracking/lib/main.es6 | 2 +- .../open-tracking/lib/open-tracking-icon.jsx | 8 ++- .../lib/open-tracking-message-status.jsx | 45 ++++++++++++++-- .../open-tracking/stylesheets/main.less | 19 ++++++- .../contenteditable/link-editor.cjsx | 1 - src/regexp-utils.coffee | 9 ++++ 19 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 internal_packages/link-tracking/assets/ic-tracking-unvisited@1x.png create mode 100644 internal_packages/link-tracking/assets/ic-tracking-unvisited@2x.png create mode 100644 internal_packages/link-tracking/assets/ic-tracking-visited@1x.png create mode 100644 internal_packages/link-tracking/assets/ic-tracking-visited@2x.png create mode 100644 internal_packages/link-tracking/lib/link-tracking-message-extension.es6 diff --git a/internal_packages/link-tracking/assets/ic-tracking-unvisited@1x.png b/internal_packages/link-tracking/assets/ic-tracking-unvisited@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e82997838f2d450ab89b76f8eb5a73e968bbca3b GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61|)m))t&+=mUKs7M+SzC{oH>NL6Qo|9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?gFHN;HUHMdLYGF z;1OBOz#ygy!i=6lDj$G?p`I>|Asp9rPjBRP31DEj@Z3B~dX8ubS6E?Z(8B$kIR%PR z!5UkvxRg5_RixI}2VRTSR1=8^WbK(DnCvJ&;c@*RJ9D-tjMBH(>=rV)!MsnSaNp`W Yh7M1G8K;YSszJ7Uy85}Sb4q9e0OsXEN&o-= literal 0 HcmV?d00001 diff --git a/internal_packages/link-tracking/assets/ic-tracking-unvisited@2x.png b/internal_packages/link-tracking/assets/ic-tracking-unvisited@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1d5597fde38a789cdc9758a7f0d517d6f4bda0 GIT binary patch literal 381 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhEa{HEjtmSN`?>!lvI6-E$sR$z z3=CC3xrP=7hF?ITh8GMBr3MTPuM!v-tY$DUh!@P+6=(yLU`z6LcLCBs@Y8vBJ&@uo z@Q5sCU=ULUVMfm&l@CC{hn_BuArhBMFWP!B2THU)OlOl8-C|T0=h?H=p!>t}jT?k= znyLl9G(~V2ap*bex*QE_p6b0r&w(RZ`PR}6*PgMlOuj6`w?}5*e5bHb^VJr|#Kbac{wi2WIaK-tOPGaduLL&gxw+ADJ~z&sZ+OwWY8(@=kc^<(W=Lc-95+{9e3E z!VmJk^$Y!#@l!kCe)G3$sQKxN zqc(X3^W38jGJZ2Sa_hyp*A)lu%y_0)7Q~}}bUwe{LxI^5XLj2>R66>BZ$I-?%Q>dX R4qrea;pyt`}GjD!FH literal 0 HcmV?d00001 diff --git a/internal_packages/link-tracking/assets/ic-tracking-visited@1x.png b/internal_packages/link-tracking/assets/ic-tracking-visited@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..ec4271ac5d0aee4988d171694d05afeaed74d538 GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^tRT$61|)m))t&+=mUKs7M+SzC{oH>NL6Qo|9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?gFHN;HUHMdLYGF z;1OBOz#ygy!i=6lDj$G?zMd|QAsp9P4<6)n2w-5i@cloNQBzUD)JdK(|IcbIo3N=# z_R9SO2amFximlY&@;NrqwZBJX01lgrnuAC6$)OWL)QTLvg_xOx` S)d@g@89ZJ6T-G@yGywo!lvI6-E$sR$z z3=CC3xrP=7hF?ITh8GMBr3MTPuM!v-tY$DUh!@P+6=(yLU`z6LcLCBs@Y8vBJ&@uo z@Q5sCU=ULUVMfm&l@CC{Q=Tr4ArhBMFB*EWIEu79%$HIwH5EGR#%?Tp*5=Trdk%Zr zOOjM}Kk!X)G!U?Da%*1D$&sYg^>UNctUYax$L(K*i(LJ?O8elwC#BNr5=p%CcO1-| z{aSs+p^AoLou8-0`)=UJKd s+;#e^&+(gC9MhOqcii;ctr+{m{F?*~rCu+|D+77i)78&qol`;+0K*)CuK)l5 literal 0 HcmV?d00001 diff --git a/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 index 74adb57af..fa60ae2bd 100644 --- a/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 +++ b/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6 @@ -1,9 +1,12 @@ -import {ComposerExtension, Actions, QuotedHTMLTransformer} from 'nylas-exports'; +import { + ComposerExtension, + Actions, + QuotedHTMLTransformer, + RegExpUtils} 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"; @@ -26,11 +29,11 @@ export default class LinkTrackingComposerExtension extends ComposerExtension { 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) => { + draftBody.unquoted = draftBody.unquoted.replace(RegExpUtils.linkTagRegex(), (match, prefix, url, suffix, content, closingTag) => { 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; + links.push({originalUrl: url, clickCount: 0, clickData: [], redirectUrl: redirectUrl}); + return prefix + redirectUrl + suffix + content + closingTag; }); // save the draft diff --git a/internal_packages/link-tracking/lib/link-tracking-icon.jsx b/internal_packages/link-tracking/lib/link-tracking-icon.jsx index 962fda3cb..63b896061 100644 --- a/internal_packages/link-tracking/lib/link-tracking-icon.jsx +++ b/internal_packages/link-tracking/lib/link-tracking-icon.jsx @@ -29,7 +29,7 @@ export default class LinkTrackingIcon extends React.Component { // 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), + clicks: sum(mostRecentMetadata.links || [], link => link.clickCount || 0), }; } return {clicks: null}; diff --git a/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 b/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 new file mode 100644 index 000000000..887b5e3ae --- /dev/null +++ b/internal_packages/link-tracking/lib/link-tracking-message-extension.es6 @@ -0,0 +1,49 @@ +import {MessageViewExtension, RegExpUtils} from 'nylas-exports' +import plugin from '../package.json' + +export default class LinkTrackingMessageExtension extends MessageViewExtension { + static formatMessageBody({message}) { + const metadata = message.metadataForPluginId(plugin.appId) || {}; + if ((metadata.links || []).length === 0) { return } + const links = {} + for (const link of metadata.links) { + links[link.redirectUrl] = link + } + + + message.body = message.body.replace(RegExpUtils.linkTagRegex(), (match, openTagPrefix, aTagHref, openTagSuffix, content, closingTag) => { + if (links[aTagHref]) { + const openTag = openTagPrefix + aTagHref + openTagSuffix + let title; + let dotSrc; + let newOpenTag; + const titleRe = /title="[^"]*"|title='[^']*'/gi; + + if (!content) { return match; } + if (content.search("link-tracking-dot") >= 0) { return match; } + + const originalUrl = links[aTagHref].originalUrl; + const dotImgSrcPrefix = "nylas://link-tracking/assets/"; + const dotStyles = "margin-left: 1px; vertical-align: super; margin-right: 2px; zoom: 0.55;" + + if (links[aTagHref].clickCount > 0) { + title = ` title="Number of clicks: ${links[aTagHref].clickCount} | ${originalUrl}" `; + dotSrc = dotImgSrcPrefix + "ic-tracking-visited@2x.png" + } else { + title = ` title="Never been clicked | ${originalUrl}" ` + dotSrc = dotImgSrcPrefix + "ic-tracking-unvisited@2x.png" + } + const dot = `` + + if (titleRe.test(openTag)) { + newOpenTag = openTag.replace(titleRe, title) + } else { + const tagLen = openTag.length + newOpenTag = openTag.slice(0, tagLen - 1) + title + openTag.slice(tagLen - 1, tagLen) + } + return newOpenTag + content + dot + closingTag + } + return match; + }) + } +} diff --git a/internal_packages/link-tracking/lib/link-tracking-panel.jsx b/internal_packages/link-tracking/lib/link-tracking-panel.jsx index 26861b54b..655a3f34e 100644 --- a/internal_packages/link-tracking/lib/link-tracking-panel.jsx +++ b/internal_packages/link-tracking/lib/link-tracking-panel.jsx @@ -25,8 +25,8 @@ export default class LinkTrackingPanel extends React.Component { _renderContents() { return this.state.links.map(link => { return ( - {link.url} - {link.click_count + " clicks"} + {link.originalUrl} + {link.clickCount + " clicks"} ) }) } diff --git a/internal_packages/link-tracking/lib/main.es6 b/internal_packages/link-tracking/lib/main.es6 index 7dfc532e3..c0dd3995b 100644 --- a/internal_packages/link-tracking/lib/main.es6 +++ b/internal_packages/link-tracking/lib/main.es6 @@ -2,7 +2,8 @@ import {ComponentRegistry, DatabaseStore, Message, ExtensionRegistry, Actions} f 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 LinkTrackingMessageExtension from './link-tracking-message-extension'; +// import LinkTrackingPanel from './link-tracking-panel'; import plugin from '../package.json' import request from 'request'; @@ -45,8 +46,9 @@ function afterDraftSend({draftClientId}) { export function activate() { ComponentRegistry.register(LinkTrackingButton, {role: 'Composer:ActionButton'}); ComponentRegistry.register(LinkTrackingIcon, {role: 'ThreadListIcon'}); - ComponentRegistry.register(LinkTrackingPanel, {role: 'message:BodyHeader'}); + // ComponentRegistry.register(LinkTrackingPanel, {role: 'message:BodyHeader'}); ExtensionRegistry.Composer.register(LinkTrackingComposerExtension); + ExtensionRegistry.MessageView.register(LinkTrackingMessageExtension); this._unlistenSendDraftSuccess = Actions.sendDraftSuccess.listen(afterDraftSend); } @@ -55,7 +57,8 @@ export function serialize() {} export function deactivate() { ComponentRegistry.unregister(LinkTrackingButton); ComponentRegistry.unregister(LinkTrackingIcon); - ComponentRegistry.unregister(LinkTrackingPanel); + // ComponentRegistry.unregister(LinkTrackingPanel); ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension); + ExtensionRegistry.MessageView.unregister(LinkTrackingMessageExtension); this._unlistenSendDraftSuccess() } diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx index 47aafa7d4..09af7fc89 100644 --- a/internal_packages/message-list/lib/message-item.cjsx +++ b/internal_packages/message-list/lib/message-item.cjsx @@ -93,15 +93,15 @@ class MessageItem extends React.Component
{@_renderHeaderSideItems()}
+ + - -
{@_renderFromParticipants()} diff --git a/internal_packages/message-list/lib/plugins/autolinker-extension.coffee b/internal_packages/message-list/lib/plugins/autolinker-extension.coffee index d59e8460c..9a88f3f27 100644 --- a/internal_packages/message-list/lib/plugins/autolinker-extension.coffee +++ b/internal_packages/message-list/lib/plugins/autolinker-extension.coffee @@ -1,5 +1,5 @@ Autolinker = require 'autolinker' -{MessageViewExtension} = require 'nylas-exports' +{RegExpUtils, MessageViewExtension} = require 'nylas-exports' class AutolinkerExtension extends MessageViewExtension @@ -10,7 +10,22 @@ class AutolinkerExtension extends MessageViewExtension # Ensure that the hrefs in the email always have alt text so you can't hide # the target of links # https://regex101.com/r/cH0qM7/1 - message.body = message.body.replace /href[ ]*=[ ]*?['"]([^'"]*)(['"]+)/gi, (match, url, quoteCharacter) => - return "#{match} title=#{quoteCharacter}#{url}#{quoteCharacter} " + titleRe = -> /title\s.*?=\s.*?['"](.*)['"]/gi + message.body = message.body.replace RegExpUtils.linkTagRegex(), (match, openTagPrefix, aTagHref, openTagSuffix, content, closingTag) => + if not content or not closingTag + return match + + openTag = openTagPrefix + aTagHref + openTagSuffix + + if titleRe().test(openTag) + oldTitle = titleRe().exec(openTag)[1] + title = """ title="#{oldTitle} | #{aTagHref}" """ + openTag = openTag.replace(titleRe(), title) + else + title = """ title="#{aTagHref}" """ + tagLen = openTag.length + openTag = openTag.slice(0, tagLen - 1) + title + openTag.slice(tagLen - 1, tagLen) + + return openTag + content + closingTag module.exports = AutolinkerExtension diff --git a/internal_packages/message-list/spec/autolinker-extension-spec.coffee b/internal_packages/message-list/spec/autolinker-extension-spec.coffee index 3329419fb..c98b08a07 100644 --- a/internal_packages/message-list/spec/autolinker-extension-spec.coffee +++ b/internal_packages/message-list/spec/autolinker-extension-spec.coffee @@ -21,8 +21,8 @@ describe "AutolinkerExtension", -> body: """
hello world! hello world! - hello world! - hello world! + hello world! + hello world! """ AutolinkerExtension.formatMessageBody({message}) expect(message.body).toEqual(expected.body) diff --git a/internal_packages/onboarding/lib/token-auth-api.coffee b/internal_packages/onboarding/lib/token-auth-api.coffee index 6c62b5f65..195bbe7b0 100644 --- a/internal_packages/onboarding/lib/token-auth-api.coffee +++ b/internal_packages/onboarding/lib/token-auth-api.coffee @@ -33,9 +33,12 @@ class TokenAuthAPI request: options, requestId: requestId }) + console.log options nodeRequest options, (error, response, body) -> statusCode = response?.statusCode + console.log error, response, body + Actions.didMakeAPIRequest({ request: options, statusCode: statusCode, diff --git a/internal_packages/open-tracking/lib/main.es6 b/internal_packages/open-tracking/lib/main.es6 index acb579e51..970280e82 100644 --- a/internal_packages/open-tracking/lib/main.es6 +++ b/internal_packages/open-tracking/lib/main.es6 @@ -25,7 +25,7 @@ function afterDraftSend({draftClientId}) { const uid = metadata.uid; // set metadata against the message - Actions.setMetadata(message, PLUGIN_ID, {open_count: 0, open_data: []}); + Actions.setMetadata(message, PLUGIN_ID, {openCount: 0, openData: []}); // post the uid and message id pair to the plugin server const data = {uid: uid, message_id: message.id, thread_id: 1}; diff --git a/internal_packages/open-tracking/lib/open-tracking-icon.jsx b/internal_packages/open-tracking/lib/open-tracking-icon.jsx index 7a31d7548..b96b3a9d4 100644 --- a/internal_packages/open-tracking/lib/open-tracking-icon.jsx +++ b/internal_packages/open-tracking/lib/open-tracking-icon.jsx @@ -20,12 +20,16 @@ export default class OpenTrackingIcon extends React.Component { _getStateFromThread(thread) { const messages = thread.metadata; + if ((messages || []).length === 0) { return {opened: false, hasMetadata: false} } const metadataObjs = messages.map(msg => msg.metadataForPluginId(plugin.appId)).filter(meta => meta); - return {opened: metadataObjs.length ? metadataObjs.every(m => m.open_count > 0) : null}; + return { + hasMetadata: metadataObjs.length > 0, + opened: metadataObjs.every(m => m.openCount > 0), + }; } _renderIcon = () => { - if (this.state.opened == null) { + if (!this.state.hasMetadata) { return ; } return this.renderImage() diff --git a/internal_packages/open-tracking/lib/open-tracking-message-status.jsx b/internal_packages/open-tracking/lib/open-tracking-message-status.jsx index 230f53e4d..b257244c6 100644 --- a/internal_packages/open-tracking/lib/open-tracking-message-status.jsx +++ b/internal_packages/open-tracking/lib/open-tracking-message-status.jsx @@ -1,13 +1,48 @@ import {React} from 'nylas-exports' -import OpenTrackingIcon from './open-tracking-icon' +import {RetinaImg} from 'nylas-component-kit' +import plugin from '../package.json' -export default class OpenTrackingMessageStatus extends OpenTrackingIcon { +export default class OpenTrackingMessageStatus extends React.Component { static displayName = "OpenTrackingMessageStatus"; - render() { - const txt = this.state.opened ? "Read" : "Unread" + static propTypes = { + message: React.PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = this._getStateFromMessage(props.message) + } + + _getStateFromMessage(message) { + const metadata = message.metadataForPluginId(plugin.appId); + if (!metadata) { + return {hasMetadata: false, opened: false} + } + return { + hasMetadata: true, + opened: metadata.openCount > 0, + }; + } + + static containerStyles = { + paddingTop: 4, + }; + + renderImage() { return ( - {this.renderImage()} {txt} + + ); + } + + render() { + if (!this.state.hasMetadata) { return false } + const txt = this.state.opened ? "Read" : "Unread"; + return ( + {this.renderImage()}  {txt} ) } } diff --git a/internal_packages/open-tracking/stylesheets/main.less b/internal_packages/open-tracking/stylesheets/main.less index ceb1ca706..8c11d67c3 100644 --- a/internal_packages/open-tracking/stylesheets/main.less +++ b/internal_packages/open-tracking/stylesheets/main.less @@ -1,11 +1,11 @@ @import "ui-variables"; @import "ui-mixins"; -.open-tracking-icon img.content-mask { +.open-tracking-icon img.content-mask.unopened { background-color: #6b777d; vertical-align: text-bottom; } -.open-tracking-icon img.content-mask.unopened { +.open-tracking-icon img.content-mask.opened { background-color: @text-color-link; } .open-tracking-icon .open-count { @@ -22,3 +22,18 @@ width: 16px; margin-right: 4px; } + +.read-receipt-message-status { + color: @text-color-very-subtle; + margin-left: 10px; + &.Unread { + img.content-mask { + background-color: @text-color-very-subtle; + } + } + &.Read { + img.content-mask { + background-color: @text-color-link; + } + } +} diff --git a/src/components/contenteditable/link-editor.cjsx b/src/components/contenteditable/link-editor.cjsx index 43e26c84c..8a33dd758 100644 --- a/src/components/contenteditable/link-editor.cjsx +++ b/src/components/contenteditable/link-editor.cjsx @@ -24,7 +24,6 @@ class LinkEditor extends React.Component componentDidMount: -> if @props.focusOnMount - console.log "FOCUSING ON MOUNT" React.findDOMNode(@refs["urlInput"]).focus() render: => diff --git a/src/regexp-utils.coffee b/src/regexp-utils.coffee index aa175849a..a8ed0197e 100644 --- a/src/regexp-utils.coffee +++ b/src/regexp-utils.coffee @@ -25,6 +25,15 @@ RegExpUtils = # This is the Gruber Regex. urlRegex: -> new RegExp(/^\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/) + # Test cases: https://regex101.com/r/jD5zC7/2 + # Retruns the following capturing groups: + # 1. start of the opening a tag to href=" + # 2. The contents of the href without quotes + # 3. the rest of the opening a tag + # 4. the contents of the a tag + # 5. the closing tag + linkTagRegex: -> new RegExp(/()([\s\S]*?)(<\/a>)/gim) + # https://regex101.com/r/zG7aW4/3 imageTagRegex: -> /]*src="([^"]*)"[^>]*>/g