mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 23:36:21 +08:00
fix(message-sidebar): New ContactCard injectable role, updated FocusedContactStore
- The FocusedContactStore was triggering too often, and leaving it up to the FullcontactStore to fetch the full Contact model for the focused contact (pulled from thread.) The FocusedContactStore triggers more responsibly, and registering for the role "MessageListSidebar:ContactCard" now gives you the focused contact as a full database model. The whole ContactCard region also fades in and out.
This commit is contained in:
parent
c73bc940b9
commit
42ad243824
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -39,17 +39,8 @@ class GithubProfile extends React.Component
|
|||
</div>
|
||||
|
||||
module.exports =
|
||||
class GithubSidebar extends React.Component
|
||||
@displayName: 'GithubSidebar'
|
||||
|
||||
# We're registering this component to appear in one of the app's primary
|
||||
# columns, the MessageListSidebar. Each React Component in a column can
|
||||
# specify a min and max width which limit the resizing behavior of the column.
|
||||
@containerStyles:
|
||||
maxWidth: 300
|
||||
minWidth: 200
|
||||
order: 2
|
||||
flexShrink: 0
|
||||
class GithubContactCardSection extends React.Component
|
||||
@displayName: 'GithubContactCardSection'
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
|
@ -1,5 +1,5 @@
|
|||
_ = require 'underscore-plus'
|
||||
GithubSidebar = require "./github-sidebar"
|
||||
GithubContactCardSection = require "./github-contact-card-section"
|
||||
{ComponentRegistry,
|
||||
WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
|
@ -11,8 +11,8 @@ module.exports =
|
|||
# Register our sidebar so that it appears in the Message List sidebar.
|
||||
# This sidebar is to the right of the Message List in both split pane mode
|
||||
# and list mode.
|
||||
ComponentRegistry.register GithubSidebar,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
ComponentRegistry.register GithubContactCardSection,
|
||||
role: "MessageListSidebar:ContactCard"
|
||||
|
||||
# Serialize is called when your package is about to be unmounted.
|
||||
# You can return a state object that will be passed back to your package
|
||||
|
@ -27,4 +27,4 @@ module.exports =
|
|||
#
|
||||
deactivate: ->
|
||||
# Unregister our component
|
||||
ComponentRegistry.unregister(GithubSidebar)
|
||||
ComponentRegistry.unregister(GithubContactCardSection)
|
0
examples/N1-Sidebar-Github/package.json → examples/N1-Github-Contact-Card-Section/package.json
Executable file → Normal file
0
examples/N1-Sidebar-Github/package.json → examples/N1-Github-Contact-Card-Section/package.json
Executable file → Normal file
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
|
@ -1,9 +1,6 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.sidebar-github-profile {
|
||||
padding: @spacing-standard;
|
||||
padding-bottom: 0;
|
||||
|
||||
a{ text-decoration: none; }
|
||||
|
||||
.logo {
|
|
@ -3,7 +3,10 @@ MessageToolbarItems = require "./message-toolbar-items"
|
|||
{ComponentRegistry,
|
||||
MessageStore,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
SidebarThreadParticipants = require "./sidebar-thread-participants"
|
||||
|
||||
{SidebarContactCard,
|
||||
SidebarSpacer,
|
||||
SidebarContactList} = require "./sidebar-components"
|
||||
|
||||
ThreadStarButton = require './thread-star-button'
|
||||
ThreadArchiveButton = require './thread-archive-button'
|
||||
|
@ -23,7 +26,11 @@ module.exports =
|
|||
ComponentRegistry.register MessageToolbarItems,
|
||||
location: WorkspaceStore.Location.MessageList.Toolbar
|
||||
|
||||
ComponentRegistry.register SidebarThreadParticipants,
|
||||
ComponentRegistry.register SidebarContactCard,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
ComponentRegistry.register SidebarSpacer,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
ComponentRegistry.register SidebarContactList,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
|
||||
ComponentRegistry.register ThreadStarButton,
|
||||
|
@ -44,7 +51,9 @@ module.exports =
|
|||
ComponentRegistry.unregister ThreadArchiveButton
|
||||
ComponentRegistry.unregister ThreadToggleUnreadButton
|
||||
ComponentRegistry.unregister MessageToolbarItems
|
||||
ComponentRegistry.unregister SidebarThreadParticipants
|
||||
ComponentRegistry.unregister SidebarContactCard
|
||||
ComponentRegistry.unregister SidebarSpacer
|
||||
ComponentRegistry.unregister SidebarContactList
|
||||
MessageStore.unregisterExtension(AutolinkerExtension)
|
||||
MessageStore.unregisterExtension(TrackingPixelsExtension)
|
||||
|
||||
|
|
111
internal_packages/message-list/lib/sidebar-components.cjsx
Normal file
111
internal_packages/message-list/lib/sidebar-components.cjsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
_ = require 'underscore'
|
||||
React = require "react"
|
||||
|
||||
{Actions, FocusedContactsStore} = require("nylas-exports")
|
||||
{TimeoutTransitionGroup,
|
||||
InjectedComponentSet,
|
||||
Flexbox} = require("nylas-component-kit")
|
||||
|
||||
class FocusedContactStorePropsContainer extends React.Component
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = FocusedContactsStore.listen(@_onChange)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
|
||||
render: ->
|
||||
classname = "sidebar-section"
|
||||
if @state.focusedContact
|
||||
classname += " visible"
|
||||
inner = React.cloneElement(@props.children, @state)
|
||||
|
||||
<div className={classname}>{inner}</div>
|
||||
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
sortedContacts: FocusedContactsStore.sortedContacts()
|
||||
focusedContact: FocusedContactsStore.focusedContact()
|
||||
|
||||
|
||||
class SidebarSpacer extends React.Component
|
||||
@displayName: 'SidebarSpacer'
|
||||
@containerStyles:
|
||||
order: 50
|
||||
flex: 1
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
render: ->
|
||||
<div style={flex: 1}></div>
|
||||
|
||||
class SidebarContactList extends React.Component
|
||||
@displayName: 'SidebarContactList'
|
||||
@containerStyles:
|
||||
order: 100
|
||||
flexShrink: 0
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
render: ->
|
||||
<FocusedContactStorePropsContainer>
|
||||
<SidebarContactListInner/>
|
||||
</FocusedContactStorePropsContainer>
|
||||
|
||||
class SidebarContactListInner extends React.Component
|
||||
constructor: (@props) ->
|
||||
|
||||
render: ->
|
||||
<div className="sidebar-contact-list">
|
||||
<h2>Thread Participants</h2>
|
||||
{@_renderSortedContacts()}
|
||||
</div>
|
||||
|
||||
_renderSortedContacts: =>
|
||||
@props.sortedContacts.map (contact) =>
|
||||
if contact.email is @props.focusedContact.email
|
||||
selected = "selected"
|
||||
else
|
||||
selected = ""
|
||||
|
||||
<div className="contact #{selected}"
|
||||
onClick={=> @_onSelectContact(contact)}
|
||||
key={contact.email + contact.name}>
|
||||
{contact.name}
|
||||
</div>
|
||||
|
||||
_onSelectContact: (contact) =>
|
||||
Actions.focusContact(contact)
|
||||
|
||||
class SidebarContactCard extends React.Component
|
||||
@displayName: 'SidebarContactCard'
|
||||
|
||||
@containerStyles:
|
||||
order: 0
|
||||
flexShrink: 0
|
||||
minWidth:200
|
||||
maxWidth:300
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
render: ->
|
||||
<FocusedContactStorePropsContainer>
|
||||
<SidebarContactCardInner />
|
||||
</FocusedContactStorePropsContainer>
|
||||
|
||||
class SidebarContactCardInner extends React.Component
|
||||
constructor: (@props) ->
|
||||
|
||||
render: ->
|
||||
<InjectedComponentSet
|
||||
className="sidebar-contact-card"
|
||||
key={@props.focusedContact.email}
|
||||
matching={role: "MessageListSidebar:ContactCard"}
|
||||
direction="column"
|
||||
exposedProps={contact: @props.focusedContact}/>
|
||||
|
||||
module.exports = {SidebarContactCard, SidebarSpacer, SidebarContactList}
|
|
@ -1,55 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
React = require "react"
|
||||
|
||||
{Actions, FocusedContactsStore} = require("nylas-exports")
|
||||
|
||||
class SidebarThreadParticipants extends React.Component
|
||||
@displayName: 'SidebarThreadParticipants'
|
||||
|
||||
@containerStyles:
|
||||
order: 3
|
||||
flexShrink: 0
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
@_getStateFromStores()
|
||||
|
||||
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.email+contact.name}>
|
||||
{contact.name}
|
||||
</div>
|
||||
)
|
||||
return contacts
|
||||
|
||||
_onSelectContact: (contact) =>
|
||||
Actions.focusContact(contact)
|
||||
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
sortedContacts: FocusedContactsStore.sortedContacts()
|
||||
focusedContact: FocusedContactsStore.focusedContact()
|
||||
|
||||
|
||||
module.exports = SidebarThreadParticipants
|
|
@ -523,22 +523,18 @@
|
|||
}
|
||||
|
||||
///////////////////////////////
|
||||
// sidebar-thread-participants.cjsx //
|
||||
// sidebar-contact-card.cjsx //
|
||||
///////////////////////////////
|
||||
.sidebar-thread-participants {
|
||||
.sidebar-section {
|
||||
opacity: 0;
|
||||
padding: @spacing-standard;
|
||||
cursor: default;
|
||||
|
||||
.other-contact {
|
||||
color: @text-color-subtle;
|
||||
font-size: @font-size-smaller;
|
||||
&.selected {
|
||||
font-weight: @font-weight-semi-bold;
|
||||
}
|
||||
&.visible {
|
||||
transition: opacity 0.1s ease-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// TODO: DRY
|
||||
h2.sidebar-h2 {
|
||||
h2 {
|
||||
font-size: 11px;
|
||||
font-weight: @font-weight-semi-bold;
|
||||
text-transform: uppercase;
|
||||
|
@ -546,8 +542,23 @@
|
|||
border-bottom: 1px solid @border-color-divider;
|
||||
margin: 2em 0 1em 0;
|
||||
}
|
||||
|
||||
.sidebar-contact-card {
|
||||
}
|
||||
.sidebar-contact-list {
|
||||
min-height:120px;
|
||||
|
||||
.contact {
|
||||
color: @text-color-subtle;
|
||||
font-size: @font-size-smaller;
|
||||
&.selected {
|
||||
font-weight: @font-weight-semi-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.column-MessageListSidebar {
|
||||
background-color: @background-off-primary;
|
||||
overflow: auto;
|
||||
|
|
|
@ -7,53 +7,32 @@ request = require 'request'
|
|||
DatabaseStore,
|
||||
FocusedContactsStore} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
FullContactStore = Reflux.createStore
|
||||
|
||||
init: ->
|
||||
@_loadContactDataFromAPI = _.debounce(_.bind(@__loadContactDataFromAPI, @), 50)
|
||||
# @_cachedContactData = {}
|
||||
@_resolvedFocusedContact = null
|
||||
@_loadFocusedContact = _.debounce(_.bind(@_loadFocusedContact, @), 20)
|
||||
@_loadFocusedContact()
|
||||
|
||||
@listenTo ContactStore, @_loadFocusedContact
|
||||
@listenTo FocusedContactsStore, @_loadFocusedContact
|
||||
|
||||
focusedContact: -> @_resolvedFocusedContact
|
||||
|
||||
# We need to pull fresh from the database so when we update data in the
|
||||
# for the contact, we get it anew.
|
||||
_loadFocusedContact: ->
|
||||
contact = FocusedContactsStore.focusedContact()
|
||||
account = AccountStore.current()
|
||||
if contact
|
||||
@_resolvedFocusedContact = contact
|
||||
DatabaseStore.findBy(Contact, {email: contact.email, accountId: account.id}).then (contact) =>
|
||||
@_resolvedFocusedContact = contact
|
||||
if contact and not contact.thirdPartyData?["FullContact"]?
|
||||
@_loadContactDataFromAPI(contact)
|
||||
@trigger()
|
||||
dataForContact: (contact) ->
|
||||
return {} unless contact
|
||||
if contact.thirdPartyData["FullContact"]
|
||||
return contact.thirdPartyData["FullContact"]
|
||||
else
|
||||
@_resolvedFocusedContact = null
|
||||
@trigger()
|
||||
@_attachFullcontactDataToContact(contact)
|
||||
return {}
|
||||
|
||||
__loadContactDataFromAPI: (contact) ->
|
||||
# Swap the url's to see real data
|
||||
_attachFullcontactDataToContact: (contact) ->
|
||||
email = contact.email.toLowerCase().trim()
|
||||
return if email.length is 0
|
||||
|
||||
url = "https://api.fullcontact.com/v2/person.json?email=#{email}&apiKey=eadcbaf0286562a"
|
||||
request url, (err, resp, data) =>
|
||||
return {} if err
|
||||
return {} if resp.statusCode != 200
|
||||
return if err
|
||||
return if resp.statusCode != 200
|
||||
try
|
||||
data = JSON.parse data
|
||||
contact = @_mergeDataIntoContact(contact, data)
|
||||
DatabaseStore.persistModel(contact).then => @trigger(@)
|
||||
data = JSON.parse(data)
|
||||
contact.title = data.organizations?[0]?["title"]
|
||||
contact.company = data.organizations?[0]?["name"]
|
||||
contact.thirdPartyData ?= {}
|
||||
contact.thirdPartyData["FullContact"] = data
|
||||
DatabaseStore.persistModel(contact)
|
||||
@trigger()
|
||||
|
||||
_mergeDataIntoContact: (contact, data) ->
|
||||
contact.title = data.organizations?[0]?["title"]
|
||||
contact.company = data.organizations?[0]?["name"]
|
||||
contact.thirdPartyData ?= {}
|
||||
contact.thirdPartyData["FullContact"] = data
|
||||
return contact
|
||||
module.exports = FullContactStore
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports =
|
|||
|
||||
activate: (@state={}) ->
|
||||
ComponentRegistry.register SidebarFullContact,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
role: "MessageListSidebar:ContactCard"
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister(SidebarFullContact)
|
||||
|
|
|
@ -9,10 +9,10 @@ class SidebarFullContactDetails extends React.Component
|
|||
|
||||
@propTypes:
|
||||
contact: React.PropTypes.object
|
||||
fullContact: React.PropTypes.object
|
||||
fullContactData: React.PropTypes.object
|
||||
|
||||
render: =>
|
||||
<div className="full-contact">
|
||||
<div className="contact-card-fullcontact">
|
||||
<div className="header">
|
||||
{@_profilePhoto()}
|
||||
<h1 className="name">{@_name()}</h1>
|
||||
|
@ -45,7 +45,7 @@ class SidebarFullContactDetails extends React.Component
|
|||
</div>
|
||||
|
||||
_profiles: =>
|
||||
profiles = @props.fullContact.socialProfiles ? []
|
||||
profiles = @props.fullContactData.socialProfiles ? []
|
||||
profiles = _.filter profiles, (p) => @_supportedProfileTypes[p.typeId]
|
||||
|
||||
_supportedProfileTypes:
|
||||
|
@ -85,7 +85,7 @@ class SidebarFullContactDetails extends React.Component
|
|||
@_title().length > 0 or @_company().length > 0
|
||||
|
||||
_name: =>
|
||||
(@props.fullContact.contactInfo?.fullName) ? @props.contact?.name ? ""
|
||||
(@props.fullContactData.contactInfo?.fullName) ? @props.contact?.name ? ""
|
||||
|
||||
_email: =>
|
||||
email = @props.contact.email ? ""
|
||||
|
@ -103,7 +103,7 @@ class SidebarFullContactDetails extends React.Component
|
|||
else return ""
|
||||
|
||||
_company: =>
|
||||
location = @props.fullContact.demographics?.locationGeneral ? ""
|
||||
location = @props.fullContactData.demographics?.locationGeneral ? ""
|
||||
name = @_primaryOrg()?.name ? ""
|
||||
if name.length > 0 and location.length > 0
|
||||
return "#{name} (#{location})"
|
||||
|
@ -114,13 +114,13 @@ class SidebarFullContactDetails extends React.Component
|
|||
else return ""
|
||||
|
||||
_primaryOrg: =>
|
||||
orgs = @props.fullContact.organizations ? []
|
||||
orgs = @props.fullContactData.organizations ? []
|
||||
org = _.findWhere orgs, isPrimary: true
|
||||
if not org? then org = orgs[0]
|
||||
return org
|
||||
|
||||
_profilePhoto: =>
|
||||
photos = @props.fullContact.photos ? []
|
||||
photos = @props.fullContactData.photos ? []
|
||||
photo = _.findWhere photo, isPrimary: true
|
||||
if not photo? then photo = _.findWhere photo, typeId: "linkedin"
|
||||
if not photo? then photo = photos[0]
|
||||
|
|
|
@ -2,47 +2,35 @@ _ = require 'underscore'
|
|||
React = require "react"
|
||||
FullContactStore = require "./fullcontact-store"
|
||||
|
||||
{InjectedComponentSet} = require 'nylas-component-kit'
|
||||
{InjectedComponentSet, TimeoutTransitionGroup} = require 'nylas-component-kit'
|
||||
|
||||
SidebarFullContactDetails = require "./sidebar-fullcontact-details"
|
||||
|
||||
class SidebarFullContact extends React.Component
|
||||
@displayName: "SidebarFullContact"
|
||||
@containerStyles:
|
||||
order: 1
|
||||
maxWidth: 300
|
||||
minWidth: 200
|
||||
flexShrink: 0
|
||||
|
||||
@propTypes:
|
||||
contact: React.PropTypes.object
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = FullContactStore.listen @_onChange
|
||||
@unsubscribe = FullContactStore.listen(@_onChange)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
|
||||
render: =>
|
||||
<div className="full-contact-sidebar">
|
||||
<SidebarFullContactDetails contact={@state.focusedContact ? {}}
|
||||
fullContact={@_fullContact()}/>
|
||||
<InjectedComponentSet matching={role: "sidebar:focusedContactInfo"}
|
||||
direction="column"
|
||||
exposedProps={focusedContact: @state.focusedContact}/>
|
||||
</div>
|
||||
|
||||
_fullContact: =>
|
||||
if @state.focusedContact?.thirdPartyData
|
||||
return @state.focusedContact?.thirdPartyData["FullContact"] ? {}
|
||||
else
|
||||
return {}
|
||||
<SidebarFullContactDetails
|
||||
contact={@props.contact}
|
||||
fullContactData={@state.focusedContactData} />
|
||||
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
focusedContact: FullContactStore.focusedContact()
|
||||
focusedContactData: FullContactStore.dataForContact(@props.contact)
|
||||
|
||||
|
||||
module.exports = SidebarFullContact
|
||||
|
|
|
@ -1,84 +1,72 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.full-contact-sidebar {
|
||||
padding: @spacing-standard;
|
||||
.contact-card-fullcontact {
|
||||
padding-bottom: 0;
|
||||
order: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
.full-contact {
|
||||
color: @text-color;
|
||||
-webkit-user-select:text;
|
||||
img.content-mask { background-color: @text-color; }
|
||||
color: @text-color;
|
||||
-webkit-user-select:text;
|
||||
img.content-mask { background-color: @text-color; }
|
||||
|
||||
h1.name {
|
||||
font-size: 20px;
|
||||
font-weight: @font-weight-normal;
|
||||
margin: 0;
|
||||
padding: 0.6em 0 0.4em 0;
|
||||
}
|
||||
|
||||
.profile-photo {
|
||||
max-width: 42px;
|
||||
max-height: 42px;
|
||||
display: block;
|
||||
float: right;
|
||||
margin-left: @spacing-standard;
|
||||
}
|
||||
|
||||
.header {
|
||||
&:before, &:after { content: " "; display: table; }
|
||||
&:after { clear: both; }
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: @text-color-subtle;
|
||||
padding: 0 0 @spacing-standard 0;
|
||||
font-size: @font-size-smaller;
|
||||
}
|
||||
|
||||
.email {
|
||||
word-break: break-all;
|
||||
}
|
||||
h1.name {
|
||||
font-size: 20px;
|
||||
font-weight: @font-weight-normal;
|
||||
margin: 0;
|
||||
padding: 0.6em 0 0.4em 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.social-profiles {
|
||||
border-top: 1px solid @border-color-divider;
|
||||
padding-top: 7px;
|
||||
}
|
||||
.social-profile {
|
||||
margin-top: 0.5em;
|
||||
.social-icon {
|
||||
margin-top: 6px;
|
||||
float: left;
|
||||
}
|
||||
.social-link {
|
||||
padding-left: @spacing-double;
|
||||
font-size: @font-size-smaller;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.profile-photo {
|
||||
max-width: 42px;
|
||||
max-height: 42px;
|
||||
display: block;
|
||||
float: right;
|
||||
margin-left: @spacing-standard;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
.header {
|
||||
&:before, &:after { content: " "; display: table; }
|
||||
&:after { clear: both; }
|
||||
}
|
||||
.sidebar-extra-info {
|
||||
font-size: 10px;
|
||||
font-weight: @font-weight-medium;
|
||||
|
||||
.subheader {
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
.sidebar-no-info {
|
||||
padding: 0 0 @spacing-standard 0;
|
||||
font-size: @font-size-smaller;
|
||||
color: fade(@text-color, 30%);
|
||||
}
|
||||
|
||||
|
||||
.email {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.social-profiles {
|
||||
border-top: 1px solid @border-color-divider;
|
||||
padding-top: 7px;
|
||||
}
|
||||
.social-profile {
|
||||
margin-top: 0.5em;
|
||||
.social-icon {
|
||||
margin-top: 6px;
|
||||
float: left;
|
||||
}
|
||||
.social-link {
|
||||
padding-left: @spacing-double;
|
||||
font-size: @font-size-smaller;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-extra-info {
|
||||
font-size: 12px;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
.sidebar-no-info {
|
||||
font-size: @font-size-smaller;
|
||||
color: fade(@text-color, 30%);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ Section: Models
|
|||
###
|
||||
class Contact extends Model
|
||||
constructor: ->
|
||||
@thirdPartyData ?= {}
|
||||
super
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
|
|
|
@ -3,65 +3,78 @@ _ = require 'underscore'
|
|||
Utils = require '../models/utils'
|
||||
Actions = require '../actions'
|
||||
NylasStore = require 'nylas-store'
|
||||
Contact = require '../models/contact'
|
||||
MessageStore = require './message-store'
|
||||
AccountStore = require './account-store'
|
||||
DatabaseStore = require './database-store'
|
||||
FocusedContentStore = require './focused-content-store'
|
||||
|
||||
# A store that handles the focuses collections of and individual contacts
|
||||
class FocusedContactsStore extends NylasStore
|
||||
constructor: ->
|
||||
@listenTo Actions.focusContact, @_focusContact
|
||||
@listenTo DatabaseStore, @_onDatabaseChanged
|
||||
@listenTo MessageStore, @_onMessageStoreChanged
|
||||
@listenTo AccountStore, @_onAccountChanged
|
||||
@listenTo FocusedContentStore, @_onFocusChanged
|
||||
|
||||
@_currentThread = null
|
||||
@_clearCurrentParticipants(silent: true)
|
||||
|
||||
@_onAccountChanged()
|
||||
@listenTo Actions.focusContact, @_onFocusContact
|
||||
@_clearCurrentParticipants()
|
||||
|
||||
sortedContacts: -> @_currentContacts
|
||||
|
||||
focusedContact: -> @_currentFocusedContact
|
||||
|
||||
_clearCurrentParticipants: ({silent}={}) ->
|
||||
@_contactScores = {}
|
||||
@_currentContacts = []
|
||||
@_currentFocusedContact = null
|
||||
@trigger() unless silent
|
||||
# We need to wait now for the MessageStore to grab all of the
|
||||
# appropriate messages for the given thread.
|
||||
|
||||
_onFocusChanged: (change) =>
|
||||
return unless change.impactsCollection('thread')
|
||||
item = FocusedContentStore.focused('thread')
|
||||
return if @_currentThread?.id is item?.id
|
||||
@_currentThread = item
|
||||
@_clearCurrentParticipants()
|
||||
@_onMessageStoreChanged()
|
||||
|
||||
# We need to wait now for the MessageStore to grab all of the
|
||||
# appropriate messages for the given thread.
|
||||
_onDatabaseChanged: (change) =>
|
||||
return unless @_currentFocusedContact
|
||||
return unless change and change.objectClass is 'contact'
|
||||
current = _.find change.objects, (c) => c.email is @_currentFocusedContact.email
|
||||
if current
|
||||
@_currentFocusedContact = current
|
||||
@trigger()
|
||||
|
||||
_onMessageStoreChanged: =>
|
||||
if MessageStore.threadId() is @_currentThread?.id
|
||||
@_setCurrentParticipants()
|
||||
else
|
||||
@_clearCurrentParticipants()
|
||||
threadId = if MessageStore.itemsLoading() then null else MessageStore.threadId()
|
||||
|
||||
_onAccountChanged: =>
|
||||
@_myEmail = (AccountStore.current()?.me().email ? "").toLowerCase().trim()
|
||||
# Always clear data immediately when we're showing the wrong thread
|
||||
if @_currentThread and @_currentThread.id isnt threadId
|
||||
@_clearCurrentParticipants()
|
||||
@trigger()
|
||||
|
||||
# Wait to populate until the user has stopped moving through threads. This is
|
||||
# important because the FocusedContactStore powers tons of third-party extensions,
|
||||
# which could do /horrible/ things when we trigger.
|
||||
@_onMessageStoreChangeThrottled ?= _.debounce =>
|
||||
thread = if MessageStore.itemsLoading() then null else MessageStore.thread()
|
||||
if thread and thread.id isnt @_currentThread?.id
|
||||
@_currentThread = thread
|
||||
@_popuateCurrentParticipants()
|
||||
, 250
|
||||
@_onMessageStoreChangeThrottled()
|
||||
|
||||
# For now we take the last message
|
||||
_setCurrentParticipants: ->
|
||||
_popuateCurrentParticipants: ->
|
||||
@_scoreAllParticipants()
|
||||
sorted = _.sortBy(_.values(@_contactScores), "score").reverse()
|
||||
@_currentContacts = _.map(sorted, (obj) -> obj.contact)
|
||||
@_focusContact(@_currentContacts[0], silent: true)
|
||||
@trigger()
|
||||
@_onFocusContact(@_currentContacts[0])
|
||||
|
||||
_focusContact: (contact, {silent}={}) =>
|
||||
return unless contact
|
||||
@_currentFocusedContact = contact
|
||||
@trigger() unless silent
|
||||
_clearCurrentParticipants: ->
|
||||
@_contactScores = {}
|
||||
@_currentContacts = []
|
||||
@_currentFocusedContact = null
|
||||
@_currentThread = null
|
||||
|
||||
_onFocusContact: (contact) =>
|
||||
if not contact
|
||||
@_currentFocusedContact = null
|
||||
@trigger()
|
||||
else
|
||||
DatabaseStore.findBy(Contact, {
|
||||
email: contact.email,
|
||||
accountId: @_currentThread.accountId
|
||||
}).then (match) =>
|
||||
@_currentFocusedContact = match ? contact
|
||||
@trigger()
|
||||
|
||||
# We score everyone to determine who's the most relevant to display in
|
||||
# the sidebar.
|
||||
|
@ -79,37 +92,34 @@ class FocusedContactsStore extends NylasStore
|
|||
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 unless contact and contact.email
|
||||
return if contact.email.trim().length is 0
|
||||
return if @_contactScores[contact.toString()]? # only assign the first time
|
||||
|
||||
penalties = @_calculatePenalties(contact, score)
|
||||
key = Utils.toEquivalentEmailForm(contact.email)
|
||||
|
||||
@_contactScores[contact.toString()] =
|
||||
@_contactScores[key] ?=
|
||||
contact: contact
|
||||
score: score - penalties
|
||||
score: score - @_calculatePenalties(contact, score)
|
||||
|
||||
_calculatePenalties: (contact, score) ->
|
||||
penalties = 0
|
||||
email = contact.email.toLowerCase().trim()
|
||||
myEmail = AccountStore.current().emailAddress
|
||||
|
||||
if email is @_myEmail
|
||||
penalties += score # The whole thing which will penalize to zero
|
||||
if email is myEmail
|
||||
# The whole thing which will penalize to zero
|
||||
penalties += score
|
||||
|
||||
notCommonDomain = not Utils.emailHasCommonDomain(@_myEmail)
|
||||
sameDomain = Utils.emailsHaveSameDomain(@_myEmail, email)
|
||||
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
|
||||
|
||||
module.exports = new FocusedContactsStore
|
||||
|
|
|
@ -140,6 +140,7 @@ class MessageStore extends NylasStore
|
|||
|
||||
_fetchFromCache: (options = {}) ->
|
||||
return unless @_thread
|
||||
|
||||
loadedThreadId = @_thread.id
|
||||
|
||||
query = DatabaseStore.findAll(Message)
|
||||
|
|
|
@ -12,7 +12,7 @@ module.exports =
|
|||
role: 'Composer:ActionButton'
|
||||
|
||||
ComponentRegistry.register MyMessageSidebar,
|
||||
role: 'sidebar:focusedContactInfo'
|
||||
role: 'MessageListSidebar:ContactCard'
|
||||
|
||||
# Serialize is called when your package is about to be unmounted.
|
||||
# You can return a state object that will be passed back to your package
|
||||
|
|
|
@ -12,8 +12,6 @@ class MyMessageSidebar extends React.Component
|
|||
# these values.
|
||||
@containerStyles:
|
||||
order: 1
|
||||
maxWidth: 300
|
||||
minWidth: 200
|
||||
flexShrink: 0
|
||||
|
||||
# This sidebar component listens to the FocusedContactStore,
|
||||
|
|
|
@ -9,7 +9,7 @@ describe "activate", ->
|
|||
spyOn(ComponentRegistry, 'register')
|
||||
activate()
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyComposerButton, {role: 'Composer:ActionButton'})
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyMessageSidebar, {role: 'sidebar:focusedContactInfo'})
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyMessageSidebar, {role: 'MessageListSidebar:ContactCard'})
|
||||
|
||||
describe "deactivate", ->
|
||||
it "should unregister the composer button and sidebar", ->
|
||||
|
|
Loading…
Reference in a new issue