diff --git a/exports/inbox-exports.coffee b/exports/inbox-exports.coffee
index e50ea0f9c..fc524a9e5 100644
--- a/exports/inbox-exports.coffee
+++ b/exports/inbox-exports.coffee
@@ -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'
diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx
index a3be90727..dcb4615ec 100644
--- a/internal_packages/message-list/lib/main.cjsx
+++ b/internal_packages/message-list/lib/main.cjsx
@@ -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'
diff --git a/internal_packages/message-list/lib/sidebar-thread-participants.cjsx b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx
new file mode 100644
index 000000000..182700b20
--- /dev/null
+++ b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx
@@ -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: ->
+
+
Thread Participants
+ {@_renderSortedContacts()}
+
+
+ _renderSortedContacts: ->
+ contacts = []
+ @state.sortedContacts.forEach (contact) =>
+ if contact is @state.focusedContact
+ selected = "selected"
+ else selected = ""
+ contacts.push(
+ @_onSelectContact(contact)}
+ key={contact.id}>
+ {contact.name}
+
+ )
+ return contacts
+
+ _onSelectContact: (contact) ->
+ Actions.focusContact(contact)
+
+ _onChange: ->
+ @setState(@_getStateFromStores())
+
+ _getStateFromStores: ->
+ sortedContacts: FocusedContactsStore.sortedContacts()
+ focusedContact: FocusedContactsStore.focusedContact()
diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less
index c4dcbdbd0..edea87ade 100644
--- a/internal_packages/message-list/stylesheets/message-list.less
+++ b/internal_packages/message-list/stylesheets/message-list.less
@@ -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;
+ }
+}
diff --git a/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee b/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee
index d9982ff5a..4c5f5be2b 100644
--- a/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee
+++ b/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee
@@ -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(@)
diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx
deleted file mode 100644
index e3f8528c9..000000000
--- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx
+++ /dev/null
@@ -1,35 +0,0 @@
-_ = require 'underscore-plus'
-React = require "react"
-
-{Actions} = require 'inbox-exports'
-
-module.exports =
-SidebarFullContactChip = React.createClass
-
- render: ->
-
- {
- 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)
- }
-
-
- _makeContactChip: (contact, compact) ->
- if contact.name == contact.email or compact == true
- @props.selectContact(contact.email)} >
-
{contact.email}
-
- else
- @props.selectContact(contact.email)} >
- {
- if compact != true
-
{contact.name}
- }
- {contact.email}
-
diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx
index f0ecb0e16..92e49cafd 100644
--- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx
+++ b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx
@@ -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
{@_title()}
{@_company()}
- {@_renderActions()}
+
+ {@_socialProfiles()}
+
+ {@_noInfo()}
- _renderActions: ->
-
-
+ _socialProfiles: ->
+ profiles = @_profiles()
+ return profiles.map (profile) =>
+
+
+ _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()
+ No additional information available.
+ 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@$2'
+ bio = profile.bio.replace(twitterRegex, replace)
+
_showSubheader: ->
@_title().length > 0 or @_company().length > 0
diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx
index 2b09dcc11..834c99bc2 100644
--- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx
+++ b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx
@@ -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
-
-
Thread Participants
- {@_renderSortedContacts()}
-
- _renderSortedContacts: ->
- contacts = []
- @state.sortedContacts.forEach (contact) =>
- if contact is @state.focusedContact
- selected = "selected"
- else selected = ""
- contacts.push(
- @_onSelectContact(contact)}
- key={contact.id}>
- {contact.name}
-
- )
- 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
diff --git a/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less b/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less
index b1c53874a..835784d3b 100644
--- a/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less
+++ b/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less
@@ -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%);
+ }
+
+
}
diff --git a/keymaps/base.cson b/keymaps/base.cson
index c0b63d9f4..74726d85c 100644
--- a/keymaps/base.cson
+++ b/keymaps/base.cson
@@ -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'
diff --git a/spec-inbox/stores/focused-contacts-store-spec.coffee b/spec-inbox/stores/focused-contacts-store-spec.coffee
new file mode 100644
index 000000000..bfa36295c
--- /dev/null
+++ b/spec-inbox/stores/focused-contacts-store-spec.coffee
@@ -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()
diff --git a/src/flux/stores/focused-contacts-store.coffee b/src/flux/stores/focused-contacts-store.coffee
new file mode 100644
index 000000000..bac2245a8
--- /dev/null
+++ b/src/flux/stores/focused-contacts-store.coffee
@@ -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
+
diff --git a/static/images/sidebar/facebook-icon@2x.png b/static/images/sidebar/facebook-icon@2x.png
new file mode 100644
index 000000000..ffd27de6c
Binary files /dev/null and b/static/images/sidebar/facebook-icon@2x.png differ
diff --git a/static/images/sidebar/icon-phone@2x.png b/static/images/sidebar/icon-phone@2x.png
new file mode 100644
index 000000000..da816e4c8
Binary files /dev/null and b/static/images/sidebar/icon-phone@2x.png differ
diff --git a/static/images/sidebar/linkedin-icon@2x.png b/static/images/sidebar/linkedin-icon@2x.png
new file mode 100644
index 000000000..539e8ab5a
Binary files /dev/null and b/static/images/sidebar/linkedin-icon@2x.png differ
diff --git a/static/images/sidebar/twitter-icon@2x.png b/static/images/sidebar/twitter-icon@2x.png
new file mode 100644
index 000000000..5df62ca21
Binary files /dev/null and b/static/images/sidebar/twitter-icon@2x.png differ