feat(sidebar): add more Salesforce states

Summary:
add more Salesforce states

more salesforce sidebar

extract focused contacts into its own store

fullcontact store fixes

extract thread participants to own module

typo

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1326
This commit is contained in:
Evan Morikawa 2015-03-20 16:31:35 -07:00
parent b005d0204b
commit 564ecca8e0
16 changed files with 323 additions and 186 deletions

View file

@ -46,6 +46,7 @@ module.exports =
WorkspaceStore: require '../src/flux/stores/workspace-store'
FileUploadStore: require '../src/flux/stores/file-upload-store'
FileDownloadStore: require '../src/flux/stores/file-download-store'
FocusedContactsStore: require '../src/flux/stores/focused-contacts-store'
## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors
SalesforceAssociation: require '../src/flux/models/salesforce-association'

View file

@ -3,6 +3,7 @@ MessageList = require "./message-list"
MessageToolbarItems = require "./message-toolbar-items"
MessageSubjectItem = require "./message-subject-item"
{ComponentRegistry, WorkspaceStore} = require 'inbox-exports'
SidebarThreadParticipants = require "./sidebar-thread-participants"
{RetinaImg} = require 'ui-components'
DownButton = React.createClass
@ -56,6 +57,10 @@ module.exports =
view: UpButton
location: WorkspaceStore.Sheet.Thread.Toolbar.Right
ComponentRegistry.register
name: 'SidebarThreadParticipants'
location: WorkspaceStore.Location.MessageListSidebar
view: SidebarThreadParticipants
deactivate: ->
ComponentRegistry.unregister 'MessageToolbarItems'

View file

@ -0,0 +1,49 @@
_ = require 'underscore-plus'
React = require "react"
{Actions, FocusedContactsStore} = require("inbox-exports")
module.exports =
SidebarThreadParticipants = React.createClass
displayName: 'SidebarThreadParticipants'
getInitialState: ->
sortedContacts: []
focusedContact: null
componentDidMount: ->
@unsubscribe = FocusedContactsStore.listen @_onChange
componentWillUnmount: ->
@unsubscribe()
render: ->
<div className="sidebar-thread-participants">
<h2 className="sidebar-h2">Thread Participants</h2>
{@_renderSortedContacts()}
</div>
_renderSortedContacts: ->
contacts = []
@state.sortedContacts.forEach (contact) =>
if contact is @state.focusedContact
selected = "selected"
else selected = ""
contacts.push(
<div className="other-contact #{selected}"
onClick={=> @_onSelectContact(contact)}
key={contact.id}>
{contact.name}
</div>
)
return contacts
_onSelectContact: (contact) ->
Actions.focusContact(contact)
_onChange: ->
@setState(@_getStateFromStores())
_getStateFromStores: ->
sortedContacts: FocusedContactsStore.sortedContacts()
focusedContact: FocusedContactsStore.focusedContact()

View file

@ -173,10 +173,6 @@
}
.column-MessageListSidebar {
background-color: @background-off-primary;
}
///////////////////////////////
// message-participants.cjsx //
///////////////////////////////
@ -235,3 +231,41 @@
}
}
}
///////////////////////////////
// sidebar-thread-participants.cjsx //
///////////////////////////////
.sidebar-thread-participants {
padding: @spacing-standard;
order: 10;
flex-shrink: 0;
.other-contact {
color: @text-color-subtle;
font-size: @font-size-smaller;
&.selected {
font-weight: @font-weight-semi-bold;
}
&:hover {cursor: pointer;}
}
// TODO: DRY
h2.sidebar-h2 {
font-size: 11px;
font-weight: @font-weight-semi-bold;
text-transform: uppercase;
color: @text-color-very-subtle;
border-bottom: 1px solid @border-color-divider;
margin: 2em 0 1em 0;
}
}
.column-MessageListSidebar {
background-color: @background-off-primary;
overflow: auto;
border-left: 1px solid #ddd;
.flexbox-handle-horizontal div {
border-right: 0;
width: 1px;
}
}

View file

@ -1,125 +1,32 @@
_ = require 'underscore-plus'
Reflux = require 'reflux'
request = require 'request'
{Utils,
Actions,
MessageStore,
NamespaceStore} = require 'inbox-exports'
{FocusedContactsStore} = require 'inbox-exports'
module.exports =
FullContactStore = Reflux.createStore
init: ->
# @listenTo Actions.getFullContactDetails, @_makeDataRequest
@listenTo Actions.selectThreadId, @_onSelectThreadId
@listenTo Actions.focusContact, @_focusContact
@listenTo MessageStore, @_onMessageStoreChanged
@listenTo NamespaceStore, @_onNamespaceChanged
@_cachedContactData = {}
@listenTo FocusedContactsStore, @_onFocusedContacts
@_currentThreadId = null
@_clearCurrentParticipants(silent: true)
@_onNamespaceChanged()
sortedContacts: -> @_currentContacts
focusedContact: -> @_currentFocusedContact
sortedContacts: -> FocusedContactsStore.sortedContacts()
focusedContact: -> FocusedContactsStore.focusedContact()
fullContactCache: ->
emails = {}
emails[contact.email] = contact for contact in @_currentContacts
contacts = FocusedContactsStore.sortedContacts()
emails[contact.email] = contact for contact in contacts
fullContactCache = {}
_.each @_cachedContactData, (fullContactData, email) ->
if email of emails then fullContactCache[email] = fullContactData
return fullContactCache
_clearCurrentParticipants: ({silent}={}) ->
@_contactScores = {}
@_currentContacts = []
@_currentFocusedContact = null
@trigger() unless silent
_onSelectThreadId: (id) ->
@_currentThreadId = id
@_clearCurrentParticipants()
# We need to wait now for the MessageStore to grab all of the
# appropriate messages for the given thread.
_onMessageStoreChanged: ->
if MessageStore.threadId() is @_currentThreadId
@_setCurrentParticipants()
else
@_clearCurrentParticipants()
_onNamespaceChanged: ->
@_myEmail = (NamespaceStore.current()?.me().email ? "").toLowerCase().trim()
# For now we take the last message
_setCurrentParticipants: ->
@_scoreAllParticipants()
sorted = _.sortBy(_.values(@_contactScores), "score").reverse()
@_currentContacts = _.map(sorted, (obj) -> obj.contact)
@_focusContact(@_currentContacts[0], silent: true)
@trigger()
_focusContact: (contact, {silent}={}) ->
return unless contact
@_currentFocusedContact = contact
_onFocusedContacts: ->
contact = FocusedContactsStore.focusedContact() ? {}
if not @_cachedContactData[contact.email]
@_fetchAPIData(contact.email)
@trigger() unless silent
# We score everyone to determine who's the most relevant to display in
# the sidebar.
_scoreAllParticipants: ->
score = (message, msgNum, field, multiplier) =>
for contact, j in (message[field] ? [])
bonus = message[field].length - j
@_assignScore(contact, (msgNum+1) * multiplier + bonus)
for message, msgNum in MessageStore.items() by -1
if message.draft
score(message, msgNum, "to", 10000)
score(message, msgNum, "cc", 1000)
else
score(message, msgNum, "from", 100)
score(message, msgNum, "to", 10)
score(message, msgNum, "cc", 1)
return @_contactScores
# Self always gets a score of 0
_assignScore: (contact, score=0) ->
return unless contact?.email
return if contact.email.trim().length is 0
return if @_contactScores[contact.nameEmail()]? # only assign the first time
penalties = @_calculatePenalties(contact, score)
@_contactScores[contact.nameEmail()] =
contact: contact
score: score - penalties
_calculatePenalties: (contact, score) ->
penalties = 0
email = contact.email.toLowerCase().trim()
if email is @_myEmail
penalties += score # The whole thing which will penalize to zero
notCommonDomain = not Utils.emailHasCommonDomain(@_myEmail)
sameDomain = Utils.emailsHaveSameDomain(@_myEmail, email)
if notCommonDomain and sameDomain
penalties += score * 0.9
return Math.max(penalties, 0)
_matchesDomain: (myEmail, email) ->
myDomain = _.last(myEmail.split("@"))
theirDomain = _.last(email.split("@"))
return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain
@trigger()
_fetchAPIData: (email) ->
# Swap the url's to see real data
@ -130,6 +37,5 @@ FullContactStore = Reflux.createStore
return {} if resp.statusCode != 200
try
data = JSON.parse data
console.log data
@_cachedContactData[email] = data
@trigger(@)

View file

@ -1,35 +0,0 @@
_ = require 'underscore-plus'
React = require "react"
{Actions} = require 'inbox-exports'
module.exports =
SidebarFullContactChip = React.createClass
render: ->
<div className="fullcontact-chips">
{
for contact in @props.contacts
if contact.name != contact.email
@_makeContactChip(contact, @props.compact)
}
{
for contact in @props.contacts
if contact.name == contact.email
@_makeContactChip(contact, @props.compact)
}
</div>
_makeContactChip: (contact, compact) ->
if contact.name == contact.email or compact == true
<div className="fullcontact-chip" onClick={=>@props.selectContact(contact.email)} >
<h6>{contact.email}</h6>
</div>
else
<div className="fullcontact-chip" onClick={=>@props.selectContact(contact.email)} >
{
if compact != true
<h3>{contact.name}</h3>
}
<h6>{contact.email}</h6>
</div>

View file

@ -2,10 +2,16 @@ _ = require 'underscore-plus'
React = require "react"
{Actions} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
module.exports =
SidebarFullContactDetails = React.createClass
_supportedProfileTypes:
twitter: true
linkedin: true
facebook: true
propTypes:
contact: React.PropTypes.object
fullContact: React.PropTypes.object
@ -21,12 +27,55 @@ SidebarFullContactDetails = React.createClass
<div className="title">{@_title()}</div>
<div className="company">{@_company()}</div>
</div>
{@_renderActions()}
<div className="social-profiles"
style={display: if @_showSocialProfiles() then "block" else "none"}>
{@_socialProfiles()}
</div>
{@_noInfo()}
</div>
_renderActions: ->
<div className="actions">
</div>
_socialProfiles: ->
profiles = @_profiles()
return profiles.map (profile) =>
<div className="social-profile">
<RetinaImg name="#{profile.typeId}-icon.png" className="social-icon" />
<div className="social-link">
<a href={profile.url}>{@_username(profile)}</a>
{@_twitterBio(profile)}
</div>
</div>
_profiles: ->
profiles = @props.fullContact.socialProfiles ? []
profiles = _.filter profiles, (p) => @_supportedProfileTypes[p.typeId]
_showSocialProfiles: ->
@_profiles().length > 0
_username: (profile) ->
if (profile.username ? "").length > 0
if profile.typeId is "twitter"
return "@#{profile.username}"
else
return profile.username
else
return profile.typeName
_noInfo: ->
if not @_showSocialProfiles() and not @_showSubheader()
<div className="sidebar-no-info">No additional information available.</div>
else return ""
_twitterBio: (profile) ->
return "" unless profile.typeId is "twitter"
return "" unless profile.bio?.length > 0
# http://stackoverflow.com/a/13398311/793472
twitterRegex = /(^|[^@\w])@(\w{1,15})\b/g
replace = '$1<a href="https://twitter.com/$2">@$2</a>'
bio = profile.bio.replace(twitterRegex, replace)
<div className="bio sidebar-extra-info"
dangerouslySetInnerHTML={{__html: bio}}></div>
_showSubheader: ->
@_title().length > 0 or @_company().length > 0

View file

@ -4,14 +4,11 @@ FullContactStore = require "./fullcontact-store"
SidebarFullContactDetails = require "./sidebar-fullcontact-details.cjsx"
{Actions} = require("inbox-exports")
module.exports =
SidebarFullContact = React.createClass
getInitialState: ->
fullContactCache: {}
sortedContacts: []
focusedContact: null
componentDidMount: ->
@ -24,30 +21,8 @@ SidebarFullContact = React.createClass
<div className="full-contact-sidebar">
<SidebarFullContactDetails contact={@state.focusedContact ? {}}
fullContact={@_fullContact()}/>
<div className="other-contacts">
<h2 className="sidebar-h2">Thread Participants</h2>
{@_renderSortedContacts()}
</div>
</div>
_renderSortedContacts: ->
contacts = []
@state.sortedContacts.forEach (contact) =>
if contact is @state.focusedContact
selected = "selected"
else selected = ""
contacts.push(
<div className="other-contact #{selected}"
onClick={=> @_onSelectContact(contact)}
key={contact.id}>
{contact.name}
</div>
)
return contacts
_onSelectContact: (contact) ->
Actions.focusContact(contact)
_fullContact: ->
if @state.focusedContact?.email
return @state.fullContactCache[@state.focusedContact.email] ? {}
@ -59,7 +34,6 @@ SidebarFullContact = React.createClass
_getStateFromStores: ->
fullContactCache: FullContactStore.fullContactCache()
sortedContacts: FullContactStore.sortedContacts()
focusedContact: FullContactStore.focusedContact()
SidebarFullContact.maxWidth = 300

View file

@ -1,6 +1,8 @@
.full-contact-sidebar {
padding: @spacing-standard;
padding-bottom: 0;
order: 1;
flex-shrink: 0;
.full-contact {
h1.name {
@ -24,31 +26,49 @@
}
.subheader {
border-top: 1px solid @border-color-divider;
margin: @spacing-standard 0;
color: @text-color-subtle;
padding: @spacing-standard 0;
padding: 0 0 @spacing-standard 0;
font-size: @font-size-smaller;
}
}
.social-profiles {
border-top: 1px solid @border-color-divider;
padding-top: 7px;
}
.social-profile {
margin-top: 0.5em;
.social-icon {
padding-top: 6px;
float: left;
}
.social-link {
padding-left: @spacing-double;
font-size: @font-size-smaller;
a {
text-decoration: none;
}
}
}
h2.sidebar-h2 {
font-size: 10px;
font-size: 11px;
font-weight: @font-weight-semi-bold;
text-transform: uppercase;
color: @text-color-very-subtle;
border-bottom: 1px solid @border-color-divider;
margin: 2em 0 1em 0;
}
.other-contacts {
.other-contact {
color: @text-color-subtle;
font-size: @font-size-smaller;
&.selected {
font-weight: @font-weight-semi-bold;
}
&:hover {cursor: pointer;}
}
.sidebar-extra-info {
font-size: 10px;
font-weight: @font-weight-medium;
color: @text-color-subtle;
}
.sidebar-no-info {
font-size: @font-size-smaller;
color: fade(@text-color, 30%);
}
}

View file

@ -24,6 +24,7 @@
'f' : 'application:forward' # Gmail
'escape': 'application:pop-sheet'
'u': 'application:pop-sheet' # Gmail
# Default cross-platform core behaviors
'left': 'core:move-left'

View file

@ -0,0 +1,24 @@
proxyquire = require 'proxyquire'
Reflux = require 'reflux'
MessageStoreStub = Reflux.createStore
items: -> []
threadId: -> null
NamespaceStoreStub = Reflux.createStore
current: -> null
FocusedContactsStore = proxyquire '../../src/flux/stores/focused-contacts-store',
"./message-store": MessageStoreStub
"./namespace-store": NamespaceStoreStub
describe "FocusedContactsStore", ->
beforeEach ->
FocusedContactsStore._currentThreadId = null
FocusedContactsStore._clearCurrentParticipants(silent: true)
it "returns no contacts with empty", ->
expect(FocusedContactsStore.sortedContacts()).toEqual []
it "returns no focused contact when empty", ->
expect(FocusedContactsStore.focusedContact()).toBeNull()

View file

@ -0,0 +1,109 @@
_ = require 'underscore-plus'
Reflux = require 'reflux'
Utils = require '../models/utils.coffee'
Actions = require '../actions'
MessageStore = require './message-store'
NamespaceStore = require './namespace-store'
# A store that handles the focuses collections of and individual contacts
module.exports =
FocusedContactsStore = Reflux.createStore
init: ->
@listenTo Actions.selectThreadId, @_onSelectThreadId
@listenTo Actions.focusContact, @_focusContact
@listenTo MessageStore, => @_onMessageStoreChanged()
@listenTo NamespaceStore, => @_onNamespaceChanged()
@_currentThreadId = null
@_clearCurrentParticipants(silent: true)
@_onNamespaceChanged()
sortedContacts: -> @_currentContacts
focusedContact: -> @_currentFocusedContact
_clearCurrentParticipants: ({silent}={}) ->
@_contactScores = {}
@_currentContacts = []
@_currentFocusedContact = null
@trigger() unless silent
_onSelectThreadId: (id) ->
@_currentThreadId = id
@_clearCurrentParticipants()
# We need to wait now for the MessageStore to grab all of the
# appropriate messages for the given thread.
_onMessageStoreChanged: ->
if MessageStore.threadId() is @_currentThreadId
@_setCurrentParticipants()
else
@_clearCurrentParticipants()
_onNamespaceChanged: ->
@_myEmail = (NamespaceStore.current()?.me().email ? "").toLowerCase().trim()
# For now we take the last message
_setCurrentParticipants: ->
@_scoreAllParticipants()
sorted = _.sortBy(_.values(@_contactScores), "score").reverse()
@_currentContacts = _.map(sorted, (obj) -> obj.contact)
@_focusContact(@_currentContacts[0], silent: true)
@trigger()
_focusContact: (contact, {silent}={}) ->
return unless contact
@_currentFocusedContact = contact
@trigger() unless silent
# We score everyone to determine who's the most relevant to display in
# the sidebar.
_scoreAllParticipants: ->
score = (message, msgNum, field, multiplier) =>
for contact, j in (message[field] ? [])
bonus = message[field].length - j
@_assignScore(contact, (msgNum+1) * multiplier + bonus)
for message, msgNum in MessageStore.items() by -1
if message.draft
score(message, msgNum, "to", 10000)
score(message, msgNum, "cc", 1000)
else
score(message, msgNum, "from", 100)
score(message, msgNum, "to", 10)
score(message, msgNum, "cc", 1)
return @_contactScores
# Self always gets a score of 0
_assignScore: (contact, score=0) ->
return unless contact?.email
return if contact.email.trim().length is 0
return if @_contactScores[contact.nameEmail()]? # only assign the first time
penalties = @_calculatePenalties(contact, score)
@_contactScores[contact.nameEmail()] =
contact: contact
score: score - penalties
_calculatePenalties: (contact, score) ->
penalties = 0
email = contact.email.toLowerCase().trim()
if email is @_myEmail
penalties += score # The whole thing which will penalize to zero
notCommonDomain = not Utils.emailHasCommonDomain(@_myEmail)
sameDomain = Utils.emailsHaveSameDomain(@_myEmail, email)
if notCommonDomain and sameDomain
penalties += score * 0.9
return Math.max(penalties, 0)
_matchesDomain: (myEmail, email) ->
myDomain = _.last(myEmail.split("@"))
theirDomain = _.last(email.split("@"))
return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B