mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-30 00:16:02 +08:00
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:
parent
b005d0204b
commit
564ecca8e0
16 changed files with 323 additions and 186 deletions
|
@ -46,6 +46,7 @@ module.exports =
|
||||||
WorkspaceStore: require '../src/flux/stores/workspace-store'
|
WorkspaceStore: require '../src/flux/stores/workspace-store'
|
||||||
FileUploadStore: require '../src/flux/stores/file-upload-store'
|
FileUploadStore: require '../src/flux/stores/file-upload-store'
|
||||||
FileDownloadStore: require '../src/flux/stores/file-download-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
|
## 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'
|
SalesforceAssociation: require '../src/flux/models/salesforce-association'
|
||||||
|
|
|
@ -3,6 +3,7 @@ MessageList = require "./message-list"
|
||||||
MessageToolbarItems = require "./message-toolbar-items"
|
MessageToolbarItems = require "./message-toolbar-items"
|
||||||
MessageSubjectItem = require "./message-subject-item"
|
MessageSubjectItem = require "./message-subject-item"
|
||||||
{ComponentRegistry, WorkspaceStore} = require 'inbox-exports'
|
{ComponentRegistry, WorkspaceStore} = require 'inbox-exports'
|
||||||
|
SidebarThreadParticipants = require "./sidebar-thread-participants"
|
||||||
{RetinaImg} = require 'ui-components'
|
{RetinaImg} = require 'ui-components'
|
||||||
|
|
||||||
DownButton = React.createClass
|
DownButton = React.createClass
|
||||||
|
@ -56,6 +57,10 @@ module.exports =
|
||||||
view: UpButton
|
view: UpButton
|
||||||
location: WorkspaceStore.Sheet.Thread.Toolbar.Right
|
location: WorkspaceStore.Sheet.Thread.Toolbar.Right
|
||||||
|
|
||||||
|
ComponentRegistry.register
|
||||||
|
name: 'SidebarThreadParticipants'
|
||||||
|
location: WorkspaceStore.Location.MessageListSidebar
|
||||||
|
view: SidebarThreadParticipants
|
||||||
|
|
||||||
deactivate: ->
|
deactivate: ->
|
||||||
ComponentRegistry.unregister 'MessageToolbarItems'
|
ComponentRegistry.unregister 'MessageToolbarItems'
|
||||||
|
|
|
@ -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()
|
|
@ -173,10 +173,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.column-MessageListSidebar {
|
|
||||||
background-color: @background-off-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
// message-participants.cjsx //
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,125 +1,32 @@
|
||||||
_ = require 'underscore-plus'
|
_ = require 'underscore-plus'
|
||||||
Reflux = require 'reflux'
|
Reflux = require 'reflux'
|
||||||
request = require 'request'
|
request = require 'request'
|
||||||
|
{FocusedContactsStore} = require 'inbox-exports'
|
||||||
{Utils,
|
|
||||||
Actions,
|
|
||||||
MessageStore,
|
|
||||||
NamespaceStore} = require 'inbox-exports'
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
FullContactStore = Reflux.createStore
|
FullContactStore = Reflux.createStore
|
||||||
|
|
||||||
init: ->
|
init: ->
|
||||||
# @listenTo Actions.getFullContactDetails, @_makeDataRequest
|
|
||||||
@listenTo Actions.selectThreadId, @_onSelectThreadId
|
|
||||||
@listenTo Actions.focusContact, @_focusContact
|
|
||||||
@listenTo MessageStore, @_onMessageStoreChanged
|
|
||||||
@listenTo NamespaceStore, @_onNamespaceChanged
|
|
||||||
|
|
||||||
@_cachedContactData = {}
|
@_cachedContactData = {}
|
||||||
|
@listenTo FocusedContactsStore, @_onFocusedContacts
|
||||||
|
|
||||||
@_currentThreadId = null
|
sortedContacts: -> FocusedContactsStore.sortedContacts()
|
||||||
@_clearCurrentParticipants(silent: true)
|
focusedContact: -> FocusedContactsStore.focusedContact()
|
||||||
|
|
||||||
@_onNamespaceChanged()
|
|
||||||
|
|
||||||
sortedContacts: -> @_currentContacts
|
|
||||||
|
|
||||||
focusedContact: -> @_currentFocusedContact
|
|
||||||
|
|
||||||
fullContactCache: ->
|
fullContactCache: ->
|
||||||
emails = {}
|
emails = {}
|
||||||
emails[contact.email] = contact for contact in @_currentContacts
|
contacts = FocusedContactsStore.sortedContacts()
|
||||||
|
emails[contact.email] = contact for contact in contacts
|
||||||
fullContactCache = {}
|
fullContactCache = {}
|
||||||
_.each @_cachedContactData, (fullContactData, email) ->
|
_.each @_cachedContactData, (fullContactData, email) ->
|
||||||
if email of emails then fullContactCache[email] = fullContactData
|
if email of emails then fullContactCache[email] = fullContactData
|
||||||
return fullContactCache
|
return fullContactCache
|
||||||
|
|
||||||
_clearCurrentParticipants: ({silent}={}) ->
|
_onFocusedContacts: ->
|
||||||
@_contactScores = {}
|
contact = FocusedContactsStore.focusedContact() ? {}
|
||||||
@_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
|
|
||||||
if not @_cachedContactData[contact.email]
|
if not @_cachedContactData[contact.email]
|
||||||
@_fetchAPIData(contact.email)
|
@_fetchAPIData(contact.email)
|
||||||
@trigger() unless silent
|
@trigger()
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
_fetchAPIData: (email) ->
|
_fetchAPIData: (email) ->
|
||||||
# Swap the url's to see real data
|
# Swap the url's to see real data
|
||||||
|
@ -130,6 +37,5 @@ FullContactStore = Reflux.createStore
|
||||||
return {} if resp.statusCode != 200
|
return {} if resp.statusCode != 200
|
||||||
try
|
try
|
||||||
data = JSON.parse data
|
data = JSON.parse data
|
||||||
console.log data
|
|
||||||
@_cachedContactData[email] = data
|
@_cachedContactData[email] = data
|
||||||
@trigger(@)
|
@trigger(@)
|
||||||
|
|
|
@ -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>
|
|
|
@ -2,10 +2,16 @@ _ = require 'underscore-plus'
|
||||||
React = require "react"
|
React = require "react"
|
||||||
|
|
||||||
{Actions} = require 'inbox-exports'
|
{Actions} = require 'inbox-exports'
|
||||||
|
{RetinaImg} = require 'ui-components'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
SidebarFullContactDetails = React.createClass
|
SidebarFullContactDetails = React.createClass
|
||||||
|
|
||||||
|
_supportedProfileTypes:
|
||||||
|
twitter: true
|
||||||
|
linkedin: true
|
||||||
|
facebook: true
|
||||||
|
|
||||||
propTypes:
|
propTypes:
|
||||||
contact: React.PropTypes.object
|
contact: React.PropTypes.object
|
||||||
fullContact: React.PropTypes.object
|
fullContact: React.PropTypes.object
|
||||||
|
@ -21,12 +27,55 @@ SidebarFullContactDetails = React.createClass
|
||||||
<div className="title">{@_title()}</div>
|
<div className="title">{@_title()}</div>
|
||||||
<div className="company">{@_company()}</div>
|
<div className="company">{@_company()}</div>
|
||||||
</div>
|
</div>
|
||||||
{@_renderActions()}
|
<div className="social-profiles"
|
||||||
|
style={display: if @_showSocialProfiles() then "block" else "none"}>
|
||||||
|
{@_socialProfiles()}
|
||||||
|
</div>
|
||||||
|
{@_noInfo()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_renderActions: ->
|
_socialProfiles: ->
|
||||||
<div className="actions">
|
profiles = @_profiles()
|
||||||
</div>
|
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: ->
|
_showSubheader: ->
|
||||||
@_title().length > 0 or @_company().length > 0
|
@_title().length > 0 or @_company().length > 0
|
||||||
|
|
|
@ -4,14 +4,11 @@ FullContactStore = require "./fullcontact-store"
|
||||||
|
|
||||||
SidebarFullContactDetails = require "./sidebar-fullcontact-details.cjsx"
|
SidebarFullContactDetails = require "./sidebar-fullcontact-details.cjsx"
|
||||||
|
|
||||||
{Actions} = require("inbox-exports")
|
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
SidebarFullContact = React.createClass
|
SidebarFullContact = React.createClass
|
||||||
|
|
||||||
getInitialState: ->
|
getInitialState: ->
|
||||||
fullContactCache: {}
|
fullContactCache: {}
|
||||||
sortedContacts: []
|
|
||||||
focusedContact: null
|
focusedContact: null
|
||||||
|
|
||||||
componentDidMount: ->
|
componentDidMount: ->
|
||||||
|
@ -24,30 +21,8 @@ SidebarFullContact = React.createClass
|
||||||
<div className="full-contact-sidebar">
|
<div className="full-contact-sidebar">
|
||||||
<SidebarFullContactDetails contact={@state.focusedContact ? {}}
|
<SidebarFullContactDetails contact={@state.focusedContact ? {}}
|
||||||
fullContact={@_fullContact()}/>
|
fullContact={@_fullContact()}/>
|
||||||
<div className="other-contacts">
|
|
||||||
<h2 className="sidebar-h2">Thread Participants</h2>
|
|
||||||
{@_renderSortedContacts()}
|
|
||||||
</div>
|
|
||||||
</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: ->
|
_fullContact: ->
|
||||||
if @state.focusedContact?.email
|
if @state.focusedContact?.email
|
||||||
return @state.fullContactCache[@state.focusedContact.email] ? {}
|
return @state.fullContactCache[@state.focusedContact.email] ? {}
|
||||||
|
@ -59,7 +34,6 @@ SidebarFullContact = React.createClass
|
||||||
|
|
||||||
_getStateFromStores: ->
|
_getStateFromStores: ->
|
||||||
fullContactCache: FullContactStore.fullContactCache()
|
fullContactCache: FullContactStore.fullContactCache()
|
||||||
sortedContacts: FullContactStore.sortedContacts()
|
|
||||||
focusedContact: FullContactStore.focusedContact()
|
focusedContact: FullContactStore.focusedContact()
|
||||||
|
|
||||||
SidebarFullContact.maxWidth = 300
|
SidebarFullContact.maxWidth = 300
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
.full-contact-sidebar {
|
.full-contact-sidebar {
|
||||||
padding: @spacing-standard;
|
padding: @spacing-standard;
|
||||||
|
padding-bottom: 0;
|
||||||
order: 1;
|
order: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.full-contact {
|
.full-contact {
|
||||||
h1.name {
|
h1.name {
|
||||||
|
@ -24,31 +26,49 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.subheader {
|
.subheader {
|
||||||
border-top: 1px solid @border-color-divider;
|
|
||||||
margin: @spacing-standard 0;
|
|
||||||
color: @text-color-subtle;
|
color: @text-color-subtle;
|
||||||
padding: @spacing-standard 0;
|
padding: 0 0 @spacing-standard 0;
|
||||||
font-size: @font-size-smaller;
|
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 {
|
h2.sidebar-h2 {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
font-weight: @font-weight-semi-bold;
|
font-weight: @font-weight-semi-bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: @text-color-very-subtle;
|
color: @text-color-very-subtle;
|
||||||
border-bottom: 1px solid @border-color-divider;
|
border-bottom: 1px solid @border-color-divider;
|
||||||
margin: 2em 0 1em 0;
|
margin: 2em 0 1em 0;
|
||||||
}
|
}
|
||||||
|
.sidebar-extra-info {
|
||||||
.other-contacts {
|
font-size: 10px;
|
||||||
.other-contact {
|
font-weight: @font-weight-medium;
|
||||||
color: @text-color-subtle;
|
color: @text-color-subtle;
|
||||||
font-size: @font-size-smaller;
|
|
||||||
&.selected {
|
|
||||||
font-weight: @font-weight-semi-bold;
|
|
||||||
}
|
|
||||||
&:hover {cursor: pointer;}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.sidebar-no-info {
|
||||||
|
font-size: @font-size-smaller;
|
||||||
|
color: fade(@text-color, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
'f' : 'application:forward' # Gmail
|
'f' : 'application:forward' # Gmail
|
||||||
|
|
||||||
'escape': 'application:pop-sheet'
|
'escape': 'application:pop-sheet'
|
||||||
|
'u': 'application:pop-sheet' # Gmail
|
||||||
|
|
||||||
# Default cross-platform core behaviors
|
# Default cross-platform core behaviors
|
||||||
'left': 'core:move-left'
|
'left': 'core:move-left'
|
||||||
|
|
24
spec-inbox/stores/focused-contacts-store-spec.coffee
Normal file
24
spec-inbox/stores/focused-contacts-store-spec.coffee
Normal 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()
|
109
src/flux/stores/focused-contacts-store.coffee
Normal file
109
src/flux/stores/focused-contacts-store.coffee
Normal 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
|
||||||
|
|
BIN
static/images/sidebar/facebook-icon@2x.png
Normal file
BIN
static/images/sidebar/facebook-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 622 B |
BIN
static/images/sidebar/icon-phone@2x.png
Normal file
BIN
static/images/sidebar/icon-phone@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 799 B |
BIN
static/images/sidebar/linkedin-icon@2x.png
Normal file
BIN
static/images/sidebar/linkedin-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 637 B |
BIN
static/images/sidebar/twitter-icon@2x.png
Normal file
BIN
static/images/sidebar/twitter-icon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 782 B |
Loading…
Add table
Reference in a new issue