mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-26 05:56:14 +08:00
feat(tracking-pixels): New MessageStore extension cuts out tracking pixels you've *sent* so you don't trigger them
Summary: - Remove thread_participants prop, we don't use them anywhere and the underscore-case is ugly. - Move autolinker into extension, update autolinker to 0.18.1 for phone number support - document message.coffee, add isFromMe() - Add tracking pixel extension that removes pixels from mail you *send*. Maybe more features later. Test Plan: Run 1 new test! (woo...) Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1787
This commit is contained in:
parent
dc79fead2b
commit
4be05ff754
20 changed files with 288 additions and 69 deletions
|
|
@ -45,6 +45,7 @@ Exports =
|
|||
# Utils
|
||||
Utils: Utils
|
||||
DOMUtils: require '../src/dom-utils'
|
||||
RegExpUtils: require '../src/regexp-utils'
|
||||
MessageUtils: require '../src/flux/models/message-utils'
|
||||
|
||||
# Mixins
|
||||
|
|
@ -58,6 +59,7 @@ Exports =
|
|||
DraftCountStore: require '../src/flux/stores/draft-count-store'
|
||||
DraftStoreExtension: require '../src/flux/stores/draft-store-extension'
|
||||
MessageStore: require '../src/flux/stores/message-store'
|
||||
MessageStoreExtension: require '../src/flux/stores/message-store-extension'
|
||||
ContactStore: require '../src/flux/stores/contact-store'
|
||||
MetadataStore: require '../src/flux/stores/metadata-store'
|
||||
NamespaceStore: require '../src/flux/stores/namespace-store'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
MessageList = require "./message-list"
|
||||
MessageToolbarItems = require "./message-toolbar-items"
|
||||
{ComponentRegistry,
|
||||
MessageStore,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
SidebarThreadParticipants = require "./sidebar-thread-participants"
|
||||
|
||||
ThreadStarButton = require './thread-star-button'
|
||||
ThreadArchiveButton = require './thread-archive-button'
|
||||
|
||||
AutolinkerExtension = require './plugins/autolinker-extension'
|
||||
TrackingPixelsExtension = require './plugins/tracking-pixels-extension'
|
||||
|
||||
module.exports =
|
||||
item: null # The DOM item the main React component renders into
|
||||
|
||||
|
|
@ -27,11 +31,16 @@ module.exports =
|
|||
ComponentRegistry.register ThreadArchiveButton,
|
||||
role: 'message:Toolbar'
|
||||
|
||||
MessageStore.registerExtension(AutolinkerExtension)
|
||||
MessageStore.registerExtension(TrackingPixelsExtension)
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister MessageList
|
||||
ComponentRegistry.unregister ThreadStarButton
|
||||
ComponentRegistry.unregister ThreadArchiveButton
|
||||
ComponentRegistry.unregister MessageToolbarItems
|
||||
ComponentRegistry.unregister SidebarThreadParticipants
|
||||
MessageStore.unregisterExtension(AutolinkerExtension)
|
||||
MessageStore.unregisterExtension(TrackingPixelsExtension)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ MessageControls = require './message-controls'
|
|||
{Utils,
|
||||
Actions,
|
||||
MessageUtils,
|
||||
MessageStore,
|
||||
QuotedHTMLParser,
|
||||
ComponentRegistry,
|
||||
FileDownloadStore} = require 'nylas-exports'
|
||||
{RetinaImg,
|
||||
InjectedComponentSet,
|
||||
InjectedComponent} = require 'nylas-component-kit'
|
||||
Autolinker = require 'autolinker'
|
||||
|
||||
TransparentPixel = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="
|
||||
MessageBodyWidth = 740
|
||||
|
|
@ -25,7 +25,6 @@ class MessageItem extends React.Component
|
|||
@propTypes =
|
||||
thread: React.PropTypes.object.isRequired
|
||||
message: React.PropTypes.object.isRequired
|
||||
thread_participants: React.PropTypes.arrayOf(React.PropTypes.object)
|
||||
collapsed: React.PropTypes.bool
|
||||
|
||||
constructor: (@props) ->
|
||||
|
|
@ -105,7 +104,6 @@ class MessageItem extends React.Component
|
|||
from={@props.message.from}
|
||||
subject={@props.message.subject}
|
||||
onClick={@_onClickParticipants}
|
||||
thread_participants={@props.thread_participants}
|
||||
isDetailed={@state.detailedHeaders}
|
||||
message_participants={@props.message.participants()} />
|
||||
|
||||
|
|
@ -166,10 +164,15 @@ class MessageItem extends React.Component
|
|||
_formatBody: =>
|
||||
return "" unless @props.message and @props.message.body
|
||||
|
||||
# Give each extension the message object to process the body, but don't
|
||||
# allow them to modify anything but the body for the time being.
|
||||
body = @props.message.body
|
||||
|
||||
# Apply the autolinker pass to make emails and links clickable
|
||||
body = Autolinker.link(body, {twitter: false})
|
||||
for extension in MessageStore.extensions()
|
||||
continue unless extension.formatMessageBody
|
||||
virtual = @props.message.clone()
|
||||
virtual.body = body
|
||||
extension.formatMessageBody(virtual)
|
||||
body = virtual.body
|
||||
|
||||
# Find inline images and give them a calculated CSS height based on
|
||||
# html width and height, when available. This means nothing changes size
|
||||
|
|
|
|||
|
|
@ -297,7 +297,6 @@ class MessageList extends React.Component
|
|||
|
||||
_messageComponents: =>
|
||||
appliedInitialScroll = false
|
||||
threadParticipants = @_threadParticipants()
|
||||
components = []
|
||||
|
||||
messages = @_messagesWithMinification(@state.messages)
|
||||
|
|
@ -335,8 +334,7 @@ class MessageList extends React.Component
|
|||
message={message}
|
||||
className={className}
|
||||
collapsed={collapsed}
|
||||
isLastMsg={(messages.length - 1 is idx)}
|
||||
thread_participants={threadParticipants} />
|
||||
isLastMsg={(messages.length - 1 is idx)} />
|
||||
|
||||
components.push @_renderReplyArea()
|
||||
|
||||
|
|
@ -445,19 +443,6 @@ class MessageList extends React.Component
|
|||
@setState(ready: true)
|
||||
@_cacheScrollPos()
|
||||
|
||||
_threadParticipants: =>
|
||||
# We calculate the list of participants instead of grabbing it from
|
||||
# `@state.currentThread.participants` because it makes it easier to
|
||||
# test, is a better source of ground truth, and saves us from more
|
||||
# dependencies.
|
||||
participants = {}
|
||||
for msg in (@state.messages ? [])
|
||||
contacts = msg.participants()
|
||||
for contact in contacts
|
||||
if contact? and contact.email?.length > 0
|
||||
participants[contact.email] = contact
|
||||
return _.values(participants)
|
||||
|
||||
_onResize: (event) =>
|
||||
@_scrollToBottom() if @_wasAtBottom()
|
||||
@_cacheScrollPos()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
Autolinker = require 'autolinker'
|
||||
{MessageStoreExtension} = require 'nylas-exports'
|
||||
|
||||
class AutolinkerExtension extends MessageStoreExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
# Apply the autolinker pass to make emails and links clickable
|
||||
message.body = Autolinker.link(message.body, {twitter: false})
|
||||
|
||||
module.exports = AutolinkerExtension
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{MessageStoreExtension, RegExpUtils} = require 'nylas-exports'
|
||||
|
||||
TrackingBlacklist = [{
|
||||
name: 'Sidekick',
|
||||
pattern: 't.signaux',
|
||||
homepage: 'http://getsidekick.com'
|
||||
}, {
|
||||
name: 'Sidekick',
|
||||
pattern: 't.senal',
|
||||
homepage: 'http://getsidekick.com'
|
||||
}, {
|
||||
name: 'Sidekick',
|
||||
pattern: 't.sidekickopen',
|
||||
homepage: 'http://getsidekick.com'
|
||||
}, {
|
||||
name: 'Sidekick',
|
||||
pattern: 't.sigopn',
|
||||
homepage: 'http://getsidekick.com'
|
||||
}, {
|
||||
name: 'Banana Tag',
|
||||
pattern: 'bl-1.com',
|
||||
homepage: 'http://bananatag.com'
|
||||
}, {
|
||||
name: 'Boomerang',
|
||||
pattern: 'mailstat.us/tr',
|
||||
homepage: 'http://boomeranggmail.com'
|
||||
}, {
|
||||
name: 'Cirrus Inisght',
|
||||
pattern: 'tracking.cirrusinsight.com',
|
||||
homepage: 'http://cirrusinsight.com'
|
||||
}, {
|
||||
name: 'Yesware',
|
||||
pattern: 'app.yesware.com',
|
||||
homepage: 'http://yesware.com'
|
||||
}, {
|
||||
name: 'Yesware',
|
||||
pattern: 't.yesware.com',
|
||||
homepage: 'http://yesware.com'
|
||||
}, {
|
||||
name: 'Streak',
|
||||
pattern: 'mailfoogae.appspot.com',
|
||||
homepage: 'http://streak.com'
|
||||
}, {
|
||||
name: 'LaunchBit',
|
||||
pattern: 'launchbit.com/taz-pixel',
|
||||
homepage: 'http://launchbit.com'
|
||||
}, {
|
||||
name: 'MailChimp',
|
||||
pattern: 'list-manage.com/track',
|
||||
homepage: 'http://mailchimp.com'
|
||||
}, {
|
||||
name: 'Postmark',
|
||||
pattern: 'cmail1.com/t',
|
||||
homepage: 'http://postmarkapp.com'
|
||||
}, {
|
||||
name: 'iContact',
|
||||
pattern: 'click.icptrack.com/icp/',
|
||||
homepage: 'http://icontact.com'
|
||||
}, {
|
||||
name: 'Infusionsoft',
|
||||
pattern: 'infusionsoft.com/app/emailOpened',
|
||||
homepage: 'http://infusionsoft.com'
|
||||
}, {
|
||||
name: 'Intercom',
|
||||
pattern: 'via.intercom.io/o',
|
||||
homepage: 'http://intercom.io'
|
||||
}, {
|
||||
name: 'Mandrill',
|
||||
pattern: 'mandrillapp.com/track',
|
||||
homepage: 'http://mandrillapp.com'
|
||||
}, {
|
||||
name: 'Hubspot',
|
||||
pattern: 't.hsms06.com',
|
||||
homepage: 'http://hubspot.com'
|
||||
}, {
|
||||
name: 'RelateIQ',
|
||||
pattern: 'app.relateiq.com/t.png',
|
||||
homepage: 'http://relateiq.com'
|
||||
}, {
|
||||
name: 'RJ Metrics',
|
||||
pattern: 'go.rjmetrics.com',
|
||||
homepage: 'http://rjmetrics.com'
|
||||
}, {
|
||||
name: 'Mixpanel',
|
||||
pattern: 'api.mixpanel.com/track',
|
||||
homepage: 'http://mixpanel.com'
|
||||
}, {
|
||||
name: 'Front App',
|
||||
pattern: 'web.frontapp.com/api',
|
||||
homepage: 'http://frontapp.com'
|
||||
}, {
|
||||
name: 'Mailtrack.io',
|
||||
pattern: 'mailtrack.io/trace',
|
||||
homepage: 'http://mailtrack.io'
|
||||
}, {
|
||||
name: 'Salesloft',
|
||||
pattern: 'sdr.salesloft.com/email_trackers',
|
||||
homepage: 'http://salesloft.com'
|
||||
}]
|
||||
|
||||
class TrackingPixelsExtension extends MessageStoreExtension
|
||||
|
||||
@formatMessageBody: (message) ->
|
||||
return unless message.isFromMe()
|
||||
|
||||
regex = RegExpUtils.imageTagRegex()
|
||||
body = message.body
|
||||
spliceRegions = []
|
||||
|
||||
# Identify img tags that should be cut out
|
||||
while (result = regex.exec(body)) isnt null
|
||||
for item in TrackingBlacklist
|
||||
if result[1].indexOf(item.pattern) > 0
|
||||
spliceRegions.push(start: result.index, end: result.index + result[0].length)
|
||||
continue
|
||||
|
||||
# Remove them all, from the end of the string to the start
|
||||
spliceRegions.reverse().forEach ({start, end}) ->
|
||||
body = body.substr(0, start) + body.substr(end)
|
||||
message.body = body
|
||||
|
||||
module.exports = TrackingPixelsExtension
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
"atom": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"autolinker": "0.18.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,8 +127,7 @@ describe "MessageItem", ->
|
|||
<MessageItem key={@message.id}
|
||||
message={@message}
|
||||
thread={@thread}
|
||||
collapsed={collapsed}
|
||||
thread_participants={@threadParticipants} />
|
||||
collapsed={collapsed} />
|
||||
)
|
||||
|
||||
# TODO: We currently don't support collapsed messages
|
||||
|
|
|
|||
|
|
@ -202,10 +202,6 @@ describe "MessageList", ->
|
|||
msgs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "message-item-wrap collapsed")
|
||||
expect(msgs.length).toBe 4
|
||||
|
||||
it "aggregates participants across all messages", ->
|
||||
expect(@messageList._threadParticipants().length).toBe 4
|
||||
expect(@messageList._threadParticipants()[0] instanceof Contact).toBe true
|
||||
|
||||
it "displays lists of participants on the page", ->
|
||||
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
||||
MessageParticipants)
|
||||
|
|
|
|||
|
|
@ -39,21 +39,6 @@ big_test_message = (new Message).fromJSON({
|
|||
|
||||
many_thread_users = [user_1].concat(many_users)
|
||||
|
||||
thread_participants = [
|
||||
(new Contact(user_1)),
|
||||
(new Contact(user_2)),
|
||||
(new Contact(user_3)),
|
||||
(new Contact(user_4))
|
||||
]
|
||||
|
||||
thread2_participants = [
|
||||
(new Contact(user_1)),
|
||||
(new Contact(user_2)),
|
||||
(new Contact(user_3)),
|
||||
(new Contact(user_4)),
|
||||
(new Contact(user_5))
|
||||
]
|
||||
|
||||
describe "MessageParticipants", ->
|
||||
describe "when collapsed", ->
|
||||
beforeEach ->
|
||||
|
|
@ -61,7 +46,6 @@ describe "MessageParticipants", ->
|
|||
<MessageParticipants to={test_message.to}
|
||||
cc={test_message.cc}
|
||||
from={test_message.from}
|
||||
thread_participants={many_thread_users}
|
||||
message_participants={test_message.participants()} />
|
||||
)
|
||||
|
||||
|
|
@ -79,7 +63,6 @@ describe "MessageParticipants", ->
|
|||
<MessageParticipants to={test_message.to}
|
||||
cc={test_message.cc}
|
||||
from={test_message.from}
|
||||
thread_participants={many_thread_users}
|
||||
isDetailed={true}
|
||||
message_participants={test_message.participants()} />
|
||||
)
|
||||
|
|
@ -100,7 +83,6 @@ describe "MessageParticipants", ->
|
|||
# <MessageParticipants to={big_test_message.to}
|
||||
# cc={big_test_message.cc}
|
||||
# from={big_test_message.from}
|
||||
# thread_participants={many_thread_users}
|
||||
# message_participants={big_test_message.participants()} />
|
||||
# )
|
||||
# expect(p1._isToEveryone()).toBe true
|
||||
|
|
@ -110,7 +92,6 @@ describe "MessageParticipants", ->
|
|||
# <MessageParticipants to={test_message.to}
|
||||
# cc={test_message.cc}
|
||||
# from={test_message.from}
|
||||
# thread_participants={thread2_participants}
|
||||
# message_participants={test_message.participants()} />
|
||||
# )
|
||||
# # this should be false because we don't count bccs
|
||||
|
|
@ -121,7 +102,6 @@ describe "MessageParticipants", ->
|
|||
# <MessageParticipants to={test_message.to}
|
||||
# cc={test_message.cc}
|
||||
# from={test_message.from}
|
||||
# thread_participants={thread_participants}
|
||||
# message_participants={test_message.participants()} />
|
||||
# )
|
||||
# # this should be false because we don't count bccs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
TrackingPixelsExtension = require '../lib/plugins/tracking-pixels-extension'
|
||||
{Message} = require 'nylas-exports'
|
||||
|
||||
testBody = """
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><p>Hey Ben,</p><p>
|
||||
I've noticed that we don't yet have an SLA in place with Nylas. Are you the right
|
||||
person to be speaking with to make sure everything is set up on that end? If not,
|
||||
could you please put me in touch with them, so that we can get you guys set up
|
||||
correctly as soon as possible?</p><p>Thanks!</p><p>Gleb Polyakov</p><p>Head of
|
||||
Business Development and Growth</p><img src="https://sdr.salesloft.com/email_trackers/8c8bea88-af43-4f66-bf78-a97ad73d7aec/open.gif" alt="" width="1" height="1">After Pixel
|
||||
"""
|
||||
testBodyProcessed = """
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"><p>Hey Ben,</p><p>
|
||||
I've noticed that we don't yet have an SLA in place with Nylas. Are you the right
|
||||
person to be speaking with to make sure everything is set up on that end? If not,
|
||||
could you please put me in touch with them, so that we can get you guys set up
|
||||
correctly as soon as possible?</p><p>Thanks!</p><p>Gleb Polyakov</p><p>Head of
|
||||
Business Development and Growth</p>After Pixel
|
||||
"""
|
||||
|
||||
describe "TrackingPixelsExtension", ->
|
||||
it "should splice tracking pixels and only run on messages by the current user", ->
|
||||
message = new Message(body: testBody)
|
||||
spyOn(message, 'isFromMe').andCallFake -> false
|
||||
TrackingPixelsExtension.formatMessageBody(message)
|
||||
expect(message.body).toEqual(testBody)
|
||||
|
||||
message = new Message(body: testBody)
|
||||
spyOn(message, 'isFromMe').andCallFake -> true
|
||||
TrackingPixelsExtension.formatMessageBody(message)
|
||||
expect(message.body).toEqual(testBodyProcessed)
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
"6to5-core": "^3.5",
|
||||
"async": "^0.9",
|
||||
"atom-keymap": "^5.1",
|
||||
"autolinker": "0.15.2",
|
||||
"bluebird": "^2.9",
|
||||
"clear-cut": "0.4.0",
|
||||
"coffee-react": "^2.0.0",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ React = require 'react/addons'
|
|||
classNames = require 'classnames'
|
||||
_ = require 'underscore'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{Utils, Contact, ContactStore} = require 'nylas-exports'
|
||||
{Utils, RegExpUtils, Contact, ContactStore} = require 'nylas-exports'
|
||||
RetinaImg = require './retina-img'
|
||||
|
||||
Token = React.createClass
|
||||
|
|
@ -308,7 +308,7 @@ TokenizingTextField = React.createClass
|
|||
|
||||
# If it looks like an email, and the last character entered was a
|
||||
# space, then let's add the input value.
|
||||
if Utils.emailRegex().test(val) and _.last(val) is " "
|
||||
if RegExpUtils.emailRegex().test(val) and _.last(val) is " "
|
||||
@_addInputValue(val[0...-1], skipNameLookup: true)
|
||||
else
|
||||
@_refreshCompletions(val)
|
||||
|
|
|
|||
|
|
@ -186,9 +186,8 @@ class Message extends Model
|
|||
file.namespaceId = @namespaceId
|
||||
return @
|
||||
|
||||
# We calculate the list of participants instead of grabbing it from
|
||||
# a parent because it is a better source of ground truth, and saves us
|
||||
# from more dependencies.
|
||||
# Public: Returns a set of uniqued message participants by combining the
|
||||
# `to`, `cc`, and `from` fields.
|
||||
participants: ->
|
||||
participants = {}
|
||||
contacts = _.union((@to ? []), (@cc ? []), (@from ? []))
|
||||
|
|
@ -197,13 +196,14 @@ class Message extends Model
|
|||
participants["#{(contact?.email ? "").toLowerCase().trim()} #{(contact?.name ? "").toLowerCase().trim()}"] = contact if contact?
|
||||
return _.values(participants)
|
||||
|
||||
# Returns a hash with `to` and `cc` keys for authoring a new draft in response
|
||||
# to this message. Takes `replyTo` and other important state into account.
|
||||
# Public: Returns a hash with `to` and `cc` keys for authoring a new draft in
|
||||
# "reply all" to this message. This method takes into account whether the
|
||||
# message is from the current user, and also looks at the replyTo field.
|
||||
participantsForReplyAll: ->
|
||||
to = []
|
||||
cc = []
|
||||
|
||||
if @from[0].email is NamespaceStore.current().emailAddress
|
||||
if @isFromMe()
|
||||
to = @to
|
||||
cc = @cc
|
||||
else
|
||||
|
|
@ -218,11 +218,14 @@ class Message extends Model
|
|||
|
||||
{to, cc}
|
||||
|
||||
# Public: Returns a hash with `to` and `cc` keys for authoring a new draft in
|
||||
# "reply" to this message. This method takes into account whether the
|
||||
# message is from the current user, and also looks at the replyTo field.
|
||||
participantsForReply: ->
|
||||
to = []
|
||||
cc = []
|
||||
|
||||
if @from[0].email is NamespaceStore.current().emailAddress
|
||||
if @isFromMe()
|
||||
to = @to
|
||||
else if @replyTo.length
|
||||
to = @replyTo
|
||||
|
|
@ -235,6 +238,14 @@ class Message extends Model
|
|||
fileIds: ->
|
||||
_.map @files, (file) -> file.id
|
||||
|
||||
# Public: Returns true if this message is from the current user's email
|
||||
# address. In the future, this method will take into account all of the
|
||||
# user's email addresses and namespaces.
|
||||
isFromMe: ->
|
||||
@from[0].email is NamespaceStore.current().emailAddress
|
||||
|
||||
# Public: Returns a plaintext version of the message body using Chromium's
|
||||
# DOMParser. Use with care.
|
||||
plainTextBody: ->
|
||||
if (@body ? "").trim().length is 0 then return ""
|
||||
(new DOMParser()).parseFromString(@body, "text/html").body.innerText
|
||||
|
|
@ -242,6 +253,9 @@ class Message extends Model
|
|||
fromContact: ->
|
||||
@from?[0] ? new Contact(name: 'Unknown', email: 'Unknown')
|
||||
|
||||
# Public: Returns the standard attribution line for this message,
|
||||
# localized for the current user.
|
||||
# ie "On Dec. 12th, 2015 at 4:00PM, Ben Gotow wrote:"
|
||||
replyAttributionLine: ->
|
||||
"On #{@formattedDate()}, #{@fromContact().messageName()} wrote:"
|
||||
|
||||
|
|
|
|||
|
|
@ -226,15 +226,6 @@ Utils =
|
|||
toMatch = domains[0]
|
||||
return _.every(domains, (domain) -> domain.length > 0 and toMatch is domain)
|
||||
|
||||
# It's important that the regex be wrapped in parens, otherwise
|
||||
# javascript's RegExp::exec method won't find anything even when the
|
||||
# regex matches!
|
||||
#
|
||||
# It's also imporant we return a fresh copy of the RegExp every time. A
|
||||
# javascript regex is stateful and multiple functions using this method
|
||||
# will cause unexpected behavior!
|
||||
emailRegex: -> new RegExp(/([a-z.A-Z0-9%+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})/g)
|
||||
|
||||
emailHasCommonDomain: (email="") ->
|
||||
domain = _.last(email.toLowerCase().trim().split("@"))
|
||||
return (Utils.commonDomains[domain] ? false)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ Reflux = require 'reflux'
|
|||
Actions = require '../actions'
|
||||
Contact = require '../models/contact'
|
||||
Utils = require '../models/utils'
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
DatabaseStore = require './database-store'
|
||||
NamespaceStore = require './namespace-store'
|
||||
_ = require 'underscore'
|
||||
|
|
@ -87,7 +88,7 @@ class ContactStore
|
|||
|
||||
parseContactsInString: (contactString, {skipNameLookup}={}) =>
|
||||
detected = []
|
||||
emailRegex = new RegExp(Utils.emailRegex())
|
||||
emailRegex = RegExpUtils.emailRegex()
|
||||
while (match = emailRegex.exec(contactString))
|
||||
email = match[0]
|
||||
name = null
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class DraftStore
|
|||
###
|
||||
|
||||
# Public: Returns the extensions registered with the DraftStore.
|
||||
extensions: (ext) =>
|
||||
extensions: =>
|
||||
@_extensions
|
||||
|
||||
# Public: Registers a new extension with the DraftStore. DraftStore extensions
|
||||
|
|
|
|||
35
src/flux/stores/message-store-extension.coffee
Normal file
35
src/flux/stores/message-store-extension.coffee
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
###
|
||||
Public: MessageStoreExtension is an abstract base class. To create MessageStoreExtension
|
||||
that customize message viewing, you should subclass {MessageStoreExtension} and
|
||||
implement the class methods your plugin needs.
|
||||
|
||||
To register your extension with the MessageStore, call {MessageStore::registerExtension}.
|
||||
When your package is being unloaded, you *must* call the corresponding
|
||||
{MessageStore::unregisterExtension} to unhook your extension.
|
||||
|
||||
```coffee
|
||||
activate: ->
|
||||
MessageStore.registerExtension(MyExtension)
|
||||
|
||||
...
|
||||
|
||||
deactivate: ->
|
||||
MessageStore.unregisterExtension(MyExtension)
|
||||
```
|
||||
|
||||
The MessageStoreExtension API does not currently expose any asynchronous or {Promise}-based APIs.
|
||||
This will likely change in the future. If you have a use-case for a Message Store extension that
|
||||
is not possible with the current API, please let us know.
|
||||
|
||||
Section: Stores
|
||||
###
|
||||
class MessageStoreExtension
|
||||
|
||||
###
|
||||
Public: Transform the message body HTML provided in `body` and return HTML
|
||||
that should be displayed for the message.
|
||||
###
|
||||
@formatMessageBody: (body) ->
|
||||
return body
|
||||
|
||||
module.exports = MessageStoreExtension
|
||||
|
|
@ -37,6 +37,32 @@ MessageStore = Reflux.createStore
|
|||
itemsLoading: ->
|
||||
@_itemsLoading
|
||||
|
||||
###
|
||||
Message Store Extensions
|
||||
###
|
||||
|
||||
# Public: Returns the extensions registered with the MessageStore.
|
||||
extensions: =>
|
||||
@_extensions
|
||||
|
||||
# Public: Registers a new extension with the MessageStore. MessageStore extensions
|
||||
# make it possible to customize message body parsing, and will do more in
|
||||
# the future.
|
||||
#
|
||||
# - `ext` A {MessageStoreExtension} instance.
|
||||
#
|
||||
registerExtension: (ext) =>
|
||||
@_extensions ?= []
|
||||
@_extensions.push(ext)
|
||||
|
||||
# Public: Unregisters the extension provided from the MessageStore.
|
||||
#
|
||||
# - `ext` A {MessageStoreExtension} instance.
|
||||
#
|
||||
unregisterExtension: (ext) =>
|
||||
@_extensions = _.without(@_extensions, ext)
|
||||
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_setStoreDefaults: ->
|
||||
|
|
|
|||
15
src/regexp-utils.coffee
Normal file
15
src/regexp-utils.coffee
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
RegExpUtils =
|
||||
|
||||
# It's important that the regex be wrapped in parens, otherwise
|
||||
# javascript's RegExp::exec method won't find anything even when the
|
||||
# regex matches!
|
||||
#
|
||||
# It's also imporant we return a fresh copy of the RegExp every time. A
|
||||
# javascript regex is stateful and multiple functions using this method
|
||||
# will cause unexpected behavior!
|
||||
emailRegex: -> new RegExp(/([a-z.A-Z0-9%+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})/g)
|
||||
|
||||
# https://regex101.com/r/zG7aW4/3
|
||||
imageTagRegex: -> /<img\s+[^>]*src="([^"]*)"[^>]*>/g
|
||||
|
||||
module.exports = RegExpUtils
|
||||
Loading…
Add table
Reference in a new issue