diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx
index 6ef52e3f3..44f34e669 100644
--- a/internal_packages/message-list/lib/main.cjsx
+++ b/internal_packages/message-list/lib/main.cjsx
@@ -5,9 +5,8 @@ MessageToolbarItems = require "./message-toolbar-items"
ExtensionRegistry,
WorkspaceStore} = require 'nylas-exports'
-{SidebarContactCard,
- SidebarSpacer,
- SidebarContactList} = require "./sidebar-components"
+SidebarPluginContainer = require "./sidebar-plugin-container"
+SidebarParticipantPicker = require './sidebar-participant-picker'
ThreadStarButton = require './thread-star-button'
ThreadArchiveButton = require './thread-archive-button'
@@ -28,11 +27,9 @@ module.exports =
ComponentRegistry.register MessageToolbarItems,
location: WorkspaceStore.Location.MessageList.Toolbar
- ComponentRegistry.register SidebarContactCard,
+ ComponentRegistry.register SidebarParticipantPicker,
location: WorkspaceStore.Location.MessageListSidebar
- ComponentRegistry.register SidebarSpacer,
- location: WorkspaceStore.Location.MessageListSidebar
- ComponentRegistry.register SidebarContactList,
+ ComponentRegistry.register SidebarPluginContainer,
location: WorkspaceStore.Location.MessageListSidebar
ComponentRegistry.register ThreadStarButton,
@@ -60,9 +57,8 @@ module.exports =
ComponentRegistry.unregister ThreadTrashButton
ComponentRegistry.unregister ThreadToggleUnreadButton
ComponentRegistry.unregister MessageToolbarItems
- ComponentRegistry.unregister SidebarContactCard
- ComponentRegistry.unregister SidebarSpacer
- ComponentRegistry.unregister SidebarContactList
+ ComponentRegistry.unregister SidebarPluginContainer
+ ComponentRegistry.unregister SidebarParticipantPicker
ExtensionRegistry.MessageView.unregister AutolinkerExtension
ExtensionRegistry.MessageView.unregister TrackingPixelsExtension
diff --git a/internal_packages/message-list/lib/sidebar-components.cjsx b/internal_packages/message-list/lib/sidebar-components.cjsx
deleted file mode 100644
index 8fd4dbfde..000000000
--- a/internal_packages/message-list/lib/sidebar-components.cjsx
+++ /dev/null
@@ -1,111 +0,0 @@
-_ = 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)
-
-
{inner}
-
- _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: ->
-
-
-class SidebarContactList extends React.Component
- @displayName: 'SidebarContactList'
- @containerStyles:
- order: 100
- flexShrink: 0
-
- constructor: (@props) ->
-
- render: ->
-
-
-
-
-class SidebarContactListInner extends React.Component
- constructor: (@props) ->
-
- render: ->
-
-
Thread Participants
- {@_renderSortedContacts()}
-
-
- _renderSortedContacts: =>
- @props.sortedContacts.map (contact) =>
- if contact.email is @props.focusedContact.email
- selected = "selected"
- else
- selected = ""
-
- @_onSelectContact(contact)}
- key={contact.email + contact.name}>
- {contact.name}
-
-
- _onSelectContact: (contact) =>
- Actions.focusContact(contact)
-
-class SidebarContactCard extends React.Component
- @displayName: 'SidebarContactCard'
-
- @containerStyles:
- order: 0
- flexShrink: 0
- minWidth:200
- maxWidth:300
-
- constructor: (@props) ->
-
- render: ->
-
-
-
-
-class SidebarContactCardInner extends React.Component
- constructor: (@props) ->
-
- render: ->
-
-
-module.exports = {SidebarContactCard, SidebarSpacer, SidebarContactList}
diff --git a/internal_packages/message-list/lib/sidebar-participant-picker.jsx b/internal_packages/message-list/lib/sidebar-participant-picker.jsx
new file mode 100644
index 000000000..3de10442a
--- /dev/null
+++ b/internal_packages/message-list/lib/sidebar-participant-picker.jsx
@@ -0,0 +1,74 @@
+/** @babel */
+
+import _ from 'underscore'
+import React from 'react';
+import {Actions, FocusedContactsStore} from 'nylas-exports'
+
+const SPLIT_KEY = "---splitvalue---"
+
+export default class SidebarParticipantPicker extends React.Component {
+ static displayName = 'SidebarParticipantPicker';
+
+ constructor(props) {
+ super(props);
+ this.props = props;
+ this._onSelectContact = this._onSelectContact.bind(this);
+ this.state = this._getStateFromStores();
+ }
+
+ componentDidMount() {
+ this._usub = FocusedContactsStore.listen(() => {
+ return this.setState(this._getStateFromStores());
+ });
+ }
+
+ componentWillUnmount() {
+ this._usub();
+ }
+
+ static containerStyles = {
+ order: 0,
+ flexShrink: 0,
+ };
+
+ _getStateFromStores() {
+ return {
+ sortedContacts: FocusedContactsStore.sortedContacts(),
+ focusedContact: FocusedContactsStore.focusedContact(),
+ };
+ }
+
+ _renderSortedContacts() {
+ return this.state.sortedContacts.map((contact) => {
+ const selected = contact.email === (this.state.focusedContact || {}).email
+ const key = contact.email + SPLIT_KEY + contact.name;
+
+ return (
+
+ )
+ });
+ }
+
+ _onSelectContact = (event) => {
+ const [email, name] = event.target.value.split(SPLIT_KEY);
+ const contact = _.filter(this.state.sortedContacts, (c) => {
+ return c.name === name && c.email === email;
+ })[0];
+ return Actions.focusContact(contact);
+ }
+
+ render() {
+ return (
+
+
+
+ )
+ }
+
+
+}
+
diff --git a/internal_packages/message-list/lib/sidebar-plugin-container.cjsx b/internal_packages/message-list/lib/sidebar-plugin-container.cjsx
new file mode 100644
index 000000000..8b1fafd42
--- /dev/null
+++ b/internal_packages/message-list/lib/sidebar-plugin-container.cjsx
@@ -0,0 +1,61 @@
+_ = require 'underscore'
+React = require "react"
+{FocusedContactsStore} = require("nylas-exports")
+{InjectedComponentSet} = require("nylas-component-kit")
+
+class FocusedContactStorePropsContainer extends React.Component
+ @displayName: 'FocusedContactStorePropsContainer'
+
+ 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)
+
+ {inner}
+
+ _onChange: =>
+ @setState(@_getStateFromStores())
+
+ _getStateFromStores: =>
+ sortedContacts: FocusedContactsStore.sortedContacts()
+ focusedContact: FocusedContactsStore.focusedContact()
+ focusedContactThreads: FocusedContactsStore.focusedContactThreads()
+
+class SidebarPluginContainer extends React.Component
+ @displayName: 'SidebarPluginContainer'
+
+ @containerStyles:
+ order: 1
+ flexShrink: 0
+ minWidth:200
+ maxWidth:300
+
+ constructor: (@props) ->
+
+ render: ->
+
+
+
+
+class SidebarPluginContainerInner extends React.Component
+ constructor: (@props) ->
+
+ render: ->
+
+
+module.exports = SidebarPluginContainer
diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less
index 24e06f3e2..43986ee4a 100644
--- a/internal_packages/message-list/stylesheets/message-list.less
+++ b/internal_packages/message-list/stylesheets/message-list.less
@@ -625,8 +625,13 @@ body.platform-win32 {
///////////////////////////////
.sidebar-section {
opacity: 0;
- padding: @spacing-standard;
+ margin: 5px;
cursor: default;
+ border: 1px solid @border-primary-bg;
+ border-radius: @border-radius-large;
+ background: @background-primary;
+ padding: 15px;
+
&.visible {
transition: opacity 0.1s ease-out;
opacity: 1;
@@ -643,22 +648,14 @@ body.platform-win32 {
.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;
- }
- }
- }
+}
+.sidebar-participant-picker {
+ padding: 10px 5px 20px 5px;
+ text-align: right;
}
-
.column-MessageListSidebar {
- background-color: @background-off-primary;
+ background-color: @background-secondary;
overflow: auto;
border-left: 1px solid @border-color-divider;
color: @text-color-subtle;
diff --git a/internal_packages/participant-profile/assets/facebook-sidebar-icon@2x.png b/internal_packages/participant-profile/assets/facebook-sidebar-icon@2x.png
new file mode 100644
index 000000000..783991749
Binary files /dev/null and b/internal_packages/participant-profile/assets/facebook-sidebar-icon@2x.png differ
diff --git a/internal_packages/participant-profile/assets/linkedin-sidebar-icon@2x.png b/internal_packages/participant-profile/assets/linkedin-sidebar-icon@2x.png
new file mode 100644
index 000000000..4dc431305
Binary files /dev/null and b/internal_packages/participant-profile/assets/linkedin-sidebar-icon@2x.png differ
diff --git a/internal_packages/participant-profile/assets/location-icon@2x.png b/internal_packages/participant-profile/assets/location-icon@2x.png
new file mode 100644
index 000000000..662cc3561
Binary files /dev/null and b/internal_packages/participant-profile/assets/location-icon@2x.png differ
diff --git a/internal_packages/participant-profile/assets/twitter-sidebar-icon@2x.png b/internal_packages/participant-profile/assets/twitter-sidebar-icon@2x.png
new file mode 100644
index 000000000..581d3a6c4
Binary files /dev/null and b/internal_packages/participant-profile/assets/twitter-sidebar-icon@2x.png differ
diff --git a/internal_packages/participant-profile/lib/clearbit-data-source.coffee b/internal_packages/participant-profile/lib/clearbit-data-source.coffee
new file mode 100644
index 000000000..74e18e890
--- /dev/null
+++ b/internal_packages/participant-profile/lib/clearbit-data-source.coffee
@@ -0,0 +1,45 @@
+# This file is in coffeescript just to use the existential operator!
+{AccountStore, EdgehillAPI} = require 'nylas-exports'
+
+module.exports = class ClearbitDataSource
+ clearbitAPI: ->
+ return "https://person.clearbit.com/v2/combined"
+
+ find: ({email}) ->
+ tok = AccountStore.tokenForAccountId(AccountStore.accounts()[0].id)
+ new Promise (resolve, reject) =>
+ EdgehillAPI.request
+ path: "/proxy/clearbit/#{@clearbitAPI()}/find?email=#{email}&api_token=#{tok}",
+ success: (body) =>
+ resolve(@parseResponse(body))
+ error: reject
+
+ # The clearbit -> Nylas adapater
+ parseResponse: (resp={}) ->
+ person = resp.person
+ return null unless person
+ cacheDate: Date.now()
+ email: person.email # Used as checksum
+ bio: person.bio ? person.twitter?.bio ? person.aboutme?.bio,
+ location: person.location ? person.geo?.city
+ currentTitle: person.employment?.title,
+ currentEmployer: person.employment?.name,
+ profilePhotoUrl: person.avatar,
+ socialProfiles: @_socialProfiles(person)
+
+ _socialProfiles: (person={}) ->
+ profiles = {}
+ if person.twitter
+ profiles.twitter =
+ handle: person.twitter.handle
+ url: "https://twitter.com/#{person.twitter.handle}"
+ if person.facebook
+ profiles.facebook =
+ handle: person.facebook.handle
+ url: "https://facebook.com/#{person.facebook.handle}"
+ if person.linkedin
+ profiles.linkedin =
+ handle: person.linkedin.handle
+ url: "https://linkedin.com/#{person.linkedin.handle}"
+
+ return profiles
diff --git a/internal_packages/participant-profile/lib/main.js b/internal_packages/participant-profile/lib/main.js
new file mode 100644
index 000000000..c21977e2a
--- /dev/null
+++ b/internal_packages/participant-profile/lib/main.js
@@ -0,0 +1,22 @@
+/** @babel */
+import {ComponentRegistry} from 'nylas-exports'
+import ParticipantProfileStore from './participant-profile-store'
+import SidebarParticipantProfile from './sidebar-participant-profile'
+import SidebarRelatedThreads from './sidebar-related-threads'
+
+export function activate() {
+ ParticipantProfileStore.activate()
+ ComponentRegistry.register(SidebarParticipantProfile, {role: 'MessageListSidebar:ContactCard'})
+ ComponentRegistry.register(SidebarRelatedThreads, {role: 'MessageListSidebar:ContactCard'})
+}
+
+export function deactivate() {
+ ComponentRegistry.unregister(SidebarParticipantProfile)
+ ComponentRegistry.unregister(SidebarRelatedThreads)
+ ParticipantProfileStore.deactivate()
+}
+
+export function serialize() {
+
+}
+
diff --git a/internal_packages/participant-profile/lib/participant-profile-store.js b/internal_packages/participant-profile/lib/participant-profile-store.js
new file mode 100644
index 000000000..63472dbc1
--- /dev/null
+++ b/internal_packages/participant-profile/lib/participant-profile-store.js
@@ -0,0 +1,66 @@
+/** @babel */
+import NylasStore from 'nylas-store'
+import ClearbitDataSource from './clearbit-data-source'
+import {Utils} from 'nylas-exports'
+
+// TODO: Back with Metadata
+const contactCache = {}
+const CACHE_SIZE = 100
+const contactCacheKeyIndex = []
+
+class ParticipantProfileStore extends NylasStore {
+ activate() {
+ this.cacheExpiry = 1000 * 60 * 60 * 24 // 1 day
+ this.dataSource = new ClearbitDataSource()
+ }
+
+ dataForContact(contact) {
+ if (!contact) {
+ return {}
+ }
+
+ if (Utils.likelyNonHumanEmail(contact.email)) {
+ return {}
+ }
+
+ if (this.inCache(contact)) {
+ return this.getCache(contact)
+ }
+
+ this.dataSource.find({email: contact.email}).then((data) => {
+ if (data.email === contact.email) {
+ this.setCache(contact, data);
+ this.trigger()
+ }
+ })
+ return {}
+ }
+
+ // TODO: Back by metadata.
+ getCache(contact) {
+ return contactCache[contact.email]
+ }
+
+ inCache(contact) {
+ const cache = contactCache[contact.email]
+ if (!cache) { return false }
+ if (!cache.cacheDate || Date.now() - cache.cacheDate > this.cacheExpiry) {
+ return false
+ }
+ return true
+ }
+
+ setCache(contact, value) {
+ contactCache[contact.email] = value
+ contactCacheKeyIndex.push(contact.email)
+ if (contactCacheKeyIndex.length > CACHE_SIZE) {
+ delete contactCache[contactCacheKeyIndex.shift()]
+ }
+ return value
+ }
+
+ deactivate() {
+ // no op
+ }
+}
+module.exports = new ParticipantProfileStore()
diff --git a/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx b/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx
new file mode 100644
index 000000000..82af8bf2e
--- /dev/null
+++ b/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx
@@ -0,0 +1,151 @@
+/** @babel */
+import _ from 'underscore'
+import React from 'react'
+import {shell} from 'electron'
+import {Utils} from 'nylas-exports'
+import {RetinaImg} from 'nylas-component-kit'
+import ParticipantProfileStore from './participant-profile-store'
+
+export default class SidebarParticipantProfile extends React.Component {
+ static displayName = "SidebarParticipantProfile";
+
+ static propTypes = {
+ contact: React.PropTypes.object,
+ contactThreads: React.PropTypes.array,
+ }
+
+ constructor(props) {
+ super(props);
+
+ /* We expect ParticipantProfileStore.dataForContact to return the
+ * following schema:
+ * {
+ * profilePhotoUrl: string
+ * bio: string
+ * location: string
+ * currentTitle: string
+ * currentEmployer: string
+ * socialProfiles: hash keyed by type: ('twitter', 'facebook' etc)
+ * url: string
+ * handle: string
+ * }
+ */
+ this.state = ParticipantProfileStore.dataForContact(props.contact)
+ }
+
+ componentDidMount() {
+ this.usub = ParticipantProfileStore.listen(() => {
+ this.setState(ParticipantProfileStore.dataForContact(this.props.contact))
+ })
+ }
+
+ componentWillUnmount() {
+ this.usub()
+ }
+
+ static containerStyles = {
+ order: 0,
+ }
+
+ _renderProfilePhoto() {
+ if (this.state.profilePhotoUrl) {
+ return (
+
+
+
+
+
+ )
+ }
+ return this._renderDefaultProfileImage()
+ }
+
+ _renderDefaultProfileImage() {
+ const hue = Utils.hueForString(this.props.contact.email);
+ const bgColor = `hsl(${hue}, 50%, 34%)`
+ const abv = this.props.contact.nameAbbreviation()
+ return (
+
+ )
+ }
+
+ _renderCorePersonalInfo() {
+ return (
+
+
{this.props.contact.fullName()}
+
{this.props.contact.email}
+
{this._renderSocialProfiles()}
+
+ )
+ }
+
+ _renderSocialProfiles() {
+ if (!this.state.socialProfiles) { return false }
+ return _.map(this.state.socialProfiles, (profile, type) => {
+ const linkFn = () => {shell.openExternal(profile.url)}
+ return (
+
+
+
+ )
+ });
+ }
+
+ _renderAdditionalInfo() {
+ return (
+
+ {this._renderCurrentJob()}
+ {this._renderBio()}
+ {this._renderLocation()}
+
+ )
+ }
+
+ _renderCurrentJob() {
+ if (!this.state.employer) { return false; }
+ let title = false;
+ if (this.state.title) {
+ title = {this.state.title},
+ }
+ return (
+ {title}{this.state.employer}
+ )
+ }
+
+ _renderBio() {
+ if (!this.state.bio) { return false; }
+ return (
+ {this.state.bio}
+ )
+ }
+
+ _renderLocation() {
+ if (!this.state.location) { return false; }
+ return (
+
+
+ {this.state.location}
+
+ )
+ }
+
+ render() {
+ return (
+
+ {this._renderProfilePhoto()}
+ {this._renderCorePersonalInfo()}
+ {this._renderAdditionalInfo()}
+
+ )
+ }
+
+}
diff --git a/internal_packages/participant-profile/lib/sidebar-related-threads.jsx b/internal_packages/participant-profile/lib/sidebar-related-threads.jsx
new file mode 100644
index 000000000..789bfba0b
--- /dev/null
+++ b/internal_packages/participant-profile/lib/sidebar-related-threads.jsx
@@ -0,0 +1,68 @@
+import React from 'react'
+import {Actions} from 'nylas-exports'
+
+export default class RelatedThreads extends React.Component {
+ static displayName = "RelatedThreads";
+
+ static propTypes = {
+ contact: React.PropTypes.object,
+ contactThreads: React.PropTypes.array,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {expanded: false}
+ this.DEFAULT_NUM = 3
+ }
+
+ static containerStyles = {
+ order: 1,
+ }
+
+ _onClick(thread) {
+ Actions.setFocus({collection: 'thread', item: thread})
+ }
+
+ _toggle = () => {
+ this.setState({expanded: !this.state.expanded})
+ }
+
+ _renderToggle() {
+ if (!this._hasToggle()) { return false; }
+ const msg = this.state.expanded ? "Collapse" : "Show more"
+ return (
+ {msg}
+ )
+ }
+
+ _hasToggle() {
+ return (this.props.contactThreads.length > this.DEFAULT_NUM)
+ }
+
+ render() {
+ let limit;
+ if (this.state.expanded) {
+ limit = this.props.contactThreads.length;
+ } else {
+ limit = Math.min(this.props.contactThreads.length, this.DEFAULT_NUM)
+ }
+
+ const height = ((limit + (this._hasToggle() ? 1 : 0)) * 31) + 5;
+ const shownThreads = this.props.contactThreads.slice(0, limit)
+ const threads = shownThreads.map((thread) => {
+ const onClick = () => { this._onClick(thread) }
+ return (
+
+ {thread.subject}
+
+ )
+ })
+
+ return (
+
+ {threads}
+ {this._renderToggle()}
+
+ )
+ }
+}
diff --git a/internal_packages/participant-profile/package.json b/internal_packages/participant-profile/package.json
new file mode 100644
index 000000000..4fde20203
--- /dev/null
+++ b/internal_packages/participant-profile/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "participant-profile",
+ "version": "0.1.0",
+ "title": "Participant Profile",
+ "description": "Information about a participant",
+ "isOptional": true,
+ "main": "lib/main",
+ "windowTypes": {
+ "default": true
+ },
+ "dependencies": {
+ "clearbit": "^1.2"
+ },
+ "license": "GPL-3.0"
+}
diff --git a/internal_packages/participant-profile/stylesheets/participant-profile.less b/internal_packages/participant-profile/stylesheets/participant-profile.less
new file mode 100644
index 000000000..1f114698e
--- /dev/null
+++ b/internal_packages/participant-profile/stylesheets/participant-profile.less
@@ -0,0 +1,101 @@
+@import 'ui-variables';
+
+.related-threads {
+ width: calc(~"100% + 30px");
+ position: relative;
+ left: -15px;
+ background: #f9fcfe;
+ border-top: 1px solid rgba(0,0,0,0.15);
+ transition: height 150ms ease-in-out;
+ top: 15px;
+ margin-top: -15px;
+ overflow: hidden;
+ border-radius: 0 0 @border-radius-large @border-radius-large;
+
+ .related-thread {
+ font-size: 12px;
+ color: #566f86;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0.5em 15px;
+ border-top: 1px solid rgba(0,0,0,0.08);
+ }
+
+ .toggle {
+ text-align: center;
+ padding: 0.5em 15px;
+ border-top: 1px solid rgba(0,0,0,0.08);
+ color: @text-color-link;
+ }
+}
+
+.participant-profile {
+ .profile-photo-wrap {
+ width: 50px;
+ height: 50px;
+ border-radius: @border-radius-base;
+ padding: 3px;
+ box-shadow: 0 0 1px rgba(0,0,0,0.5);
+ position: absolute;
+ left: calc(~"50% - 25px");
+ top: -31px;
+ background: @background-primary;
+
+ .profile-photo {
+ border-radius: @border-radius-small;
+ overflow: hidden;
+ text-align: center;
+ width: 44px;
+ height: 44px;
+
+ img, .default-profile-image {
+ width: 44px;
+ height: 44px;
+ }
+
+ .default-profile-image {
+ line-height: 44px;
+ font-size: 22px;
+ font-weight: 500;
+ color: white;
+ }
+ }
+ }
+
+ .core-personal-info {
+ padding-top: 30px;
+ text-align: center;
+ .full-name {
+ font-size: 16px;
+ }
+ .email {
+ color: @text-color-very-subtle;
+ }
+ .social-profiles-wrap {
+ padding: 10px;
+ }
+ .social-profile-item {
+ margin: 0 10px;
+ }
+ }
+ .additional-info {
+ font-size: 12px;
+ p {
+ margin-bottom: 15px;
+ }
+ .bio {
+ color: @text-color-very-subtle;
+ }
+ }
+}
+
+body.platform-win32 {
+ .participant-profile {
+ * {
+ border-radius: 0;
+ }
+ border-radius: 0;
+ }
+}
diff --git a/internal_packages/sidebar-fullcontact/lib/main.cjsx b/internal_packages/sidebar-fullcontact/lib/main.cjsx
index ebc0c8b49..fbd4080dd 100644
--- a/internal_packages/sidebar-fullcontact/lib/main.cjsx
+++ b/internal_packages/sidebar-fullcontact/lib/main.cjsx
@@ -7,10 +7,10 @@ module.exports =
item: null
activate: (@state={}) ->
- ComponentRegistry.register SidebarFullContact,
- role: "MessageListSidebar:ContactCard"
+ # ComponentRegistry.register SidebarFullContact,
+ # role: "MessageListSidebar:ContactCard"
deactivate: ->
- ComponentRegistry.unregister(SidebarFullContact)
+ # ComponentRegistry.unregister(SidebarFullContact)
serialize: -> @state
diff --git a/src/browser/native-notification-manager.coffee b/src/browser/native-notification-manager.coffee
deleted file mode 100644
index b208b40da..000000000
--- a/src/browser/native-notification-manager.coffee
+++ /dev/null
@@ -1,95 +0,0 @@
-# ipcMain = require 'ipcMain'
-# BrowserWindow = require 'browser-window'
-#
-# class NativeNotificationManagerUnavailable
-#
-# class NativeNotificationManagerWindows
-# constructor: ->
-#
-# class NativeNotificationManagerMacOSX
-# constructor: ->
-# @$ = require('nodobjc')
-# @$.framework('Foundation')
-#
-# @_lastNotifId = 1
-#
-# @_center = @$.NSUserNotificationCenter('defaultUserNotificationCenter')
-# @_center('removeAllDeliveredNotifications')
-#
-# Delegate = @$.NSObject.extend('NylasNotificationDelegate')
-# Delegate.addMethod('userNotificationCenter:didActivateNotification:', [@$.void, [Delegate, @$.selector, @$.id, @$.id]], @didActivateNotification)
-# Delegate.addMethod('userNotificationCenter:shouldPresentNotification:', ['c', [Delegate, @$.selector, @$.id, @$.id]], @shouldPresentNotification)
-# Delegate.register()
-# @_delegate = Delegate('alloc')('init')
-# @_center('setDelegate', @_delegate)
-#
-# # Ensure that these objects are never, ever garbage collected
-# global.__nativeNotificationManagerMacOSXDelegate = Delegate
-# global.__nativeNotificationManagerMacOSX = @
-#
-# ipcMain.on('fire-native-notification', @onFireNotification)
-#
-# shouldPresentNotification: (self, _cmd, center, notif) =>
-# return true
-#
-# didActivateNotification: (self, _cmd, center, notif) =>
-# center("removeDeliveredNotification", notif)
-#
-# [header, id, tag] = (""+notif('identifier')).split(':::')
-#
-# # Avoid potential conflicts with other libraries that may have pushed
-# # notifications on our behalf.
-# return unless header is 'N1'
-#
-# NSUserNotificationActivationType = [
-# "none",
-# "contents-clicked",
-# "action-clicked",
-# "replied",
-# "additional-action-clicked"
-# ]
-#
-# payload =
-# tag: tag
-# activationType: NSUserNotificationActivationType[(""+notif('activationType'))/1]
-#
-# if payload.activationType is "replied"
-# payload.response = (""+notif('response')).replace("{\n}", '')
-# if payload.response is "null"
-# payload.response = null
-#
-# console.log("Received notification: " + JSON.stringify(payload))
-# BrowserWindow.getAllWindows().forEach (win) ->
-# win.webContents.send('activate-native-notification', payload)
-#
-# onFireNotification: (event, {title, subtitle, body, tag, canReply}) =>
-# # By default on Mac OS X, delivering another notification with the same identifier
-# # triggers an update, which does not re-display the notification. To make subsequent
-# # calls with the same `tag` redisplay the notification, we:
-#
-# # 1. Assign each notification a unique identifier, so it's not considered an update
-# identifier = "N1:::#{@_lastNotifId}:::#{tag}"
-# @_lastNotifId += 1
-#
-# # 2. Manually remove any previous notification with the same tag
-# delivered = @_center("deliveredNotifications")
-# for existing in delivered
-# [x, x, existingTag] = (""+existing('identifier')).split(':::')
-# if existingTag is tag
-# @_center('removeDeliveredNotification', existing)
-#
-# # 3. Fire a new notification
-# notification = @$.NSUserNotification('alloc')('init')
-# notification('setTitle', @$.NSString('stringWithUTF8String', title))
-# notification('setIdentifier', @$.NSString('stringWithUTF8String', identifier))
-# notification('setSubtitle', @$.NSString('stringWithUTF8String', subtitle)) if subtitle
-# notification('setInformativeText', @$.NSString('stringWithUTF8String', body)) if body
-# notification('setHasReplyButton', canReply)
-# @_center('deliverNotification', notification)
-#
-# if process.platform is 'darwin'
-# module.exports = NativeNotificationManagerMacOSX
-# else if process.platform is 'win32'
-# module.exports = NativeNotificationManagerWindows
-# else
-# module.exports = NativeNotificationManagerUnavailable
diff --git a/src/flux/attributes/attribute-collection.coffee b/src/flux/attributes/attribute-collection.coffee
index a09e98754..118e8198b 100644
--- a/src/flux/attributes/attribute-collection.coffee
+++ b/src/flux/attributes/attribute-collection.coffee
@@ -32,9 +32,10 @@ The value of this attribute is always an array of other model objects.
Section: Database
###
class AttributeCollection extends Attribute
- constructor: ({modelKey, jsonKey, itemClass}) ->
+ constructor: ({modelKey, jsonKey, itemClass, joinOnField}) ->
super
@itemClass = itemClass
+ @joinOnField = joinOnField
@
toJSON: (vals) ->
diff --git a/src/flux/models/contact.coffee b/src/flux/models/contact.coffee
index 766e80065..acdd7f3c7 100644
--- a/src/flux/models/contact.coffee
+++ b/src/flux/models/contact.coffee
@@ -98,13 +98,13 @@ class Contact extends Model
account = AccountStore.accountForEmail(@email)
return account?
- isMePhrase: ({includeAccountLabel} = {}) ->
+ isMePhrase: ({includeAccountLabel, forceAccountLabel} = {}) ->
account = AccountStore.accountForEmail(@email)
return null unless account
if includeAccountLabel
FocusedPerspectiveStore ?= require '../stores/focused-perspective-store'
- if account and FocusedPerspectiveStore.current().accountIds.length > 1
+ if account and (FocusedPerspectiveStore.current().accountIds.length > 1 || forceAccountLabel)
return "You (#{account.label})"
return "You"
@@ -120,14 +120,17 @@ class Contact extends Model
# - compact: If the contact has a name, make the name as short as possible
# (generally returns just the first name.)
#
- displayName: ({includeAccountLabel, compact} = {}) ->
+ displayName: ({includeAccountLabel, compact, forceAccountLabel} = {}) ->
compact ?= false
includeAccountLabel ?= !compact
if compact
- @isMePhrase({includeAccountLabel}) ? @firstName()
+ @isMePhrase({includeAccountLabel, forceAccountLabel}) ? @firstName()
else
- @isMePhrase({includeAccountLabel}) ? @_nameParts().join(' ')
+ @isMePhrase({includeAccountLabel, forceAccountLabel}) ? @fullName()
+
+ fullName: ->
+ return @_nameParts().join(' ')
firstName: ->
articles = ['a', 'the']
@@ -139,6 +142,11 @@ class Contact extends Model
lastName: ->
@_nameParts()[1..-1]?.join(" ") ? ""
+ nameAbbreviation: ->
+ c1 = @firstName()[0]?.toUpperCase() ? ""
+ c2 = @lastName()[0]?.toUpperCase() ? ""
+ return c1+c2
+
_nameParts: ->
name = @name
diff --git a/src/flux/models/thread.coffee b/src/flux/models/thread.coffee
index 9ac3f63d4..a572ff2d0 100644
--- a/src/flux/models/thread.coffee
+++ b/src/flux/models/thread.coffee
@@ -66,6 +66,8 @@ class Thread extends ModelWithMetadata
itemClass: Category
'participants': Attributes.Collection
+ queryable: true
+ joinOnField: 'email'
modelKey: 'participants'
itemClass: Contact
diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee
index be77107d7..d4af5d78c 100644
--- a/src/flux/models/utils.coffee
+++ b/src/flux/models/utils.coffee
@@ -507,3 +507,32 @@ Utils =
catch err
console.error("JSON parse error: #{err}")
return data
+
+ hueForString: (str='') ->
+ str.split('').map((c) -> c.charCodeAt()).reduce((n,a) -> n+a) % 360
+
+ # Emails that nave no-reply or similar phrases in them are likely not a
+ # human. As such it's not worth the cost to do a lookup on that person.
+ #
+ # Also emails that are really long are likely computer-generated email
+ # strings used for bcc-based automated teasks.
+ likelyNonHumanEmail: (email) ->
+ prefixes = [
+ "noreply"
+ "no-reply"
+ "donotreply"
+ "do-not-reply"
+ "bounce[s]?@"
+ "notification[s]?@"
+ "support@"
+ "alert[s]?@"
+ "news@"
+ "automated@"
+ "list[s]?@"
+ "distribute[s]?@"
+ "catchall@"
+ "catch-all@"
+ ]
+ reStr = "(#{prefixes.join("|")})"
+ re = new RegExp(reStr, "gi")
+ return re.test(email) or email.length > 64
diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee
index 8899bfc80..1113f9e1e 100644
--- a/src/flux/stores/database-store.coffee
+++ b/src/flux/stores/database-store.coffee
@@ -16,7 +16,7 @@ DatabaseTransaction = require './database-transaction'
{ipcRenderer} = require 'electron'
-DatabaseVersion = 19
+DatabaseVersion = 20
DatabasePhase =
Setup: 'setup'
Ready: 'ready'
diff --git a/src/flux/stores/database-transaction.coffee b/src/flux/stores/database-transaction.coffee
index 880892980..0ca036ea6 100644
--- a/src/flux/stores/database-transaction.coffee
+++ b/src/flux/stores/database-transaction.coffee
@@ -188,7 +188,7 @@ class DatabaseTransaction
promises.push @_query("REPLACE INTO `#{klass.name}` (#{columnsSQL}) VALUES #{marksSQL}", values)
# For each join table property, find all the items in the join table for this
- # model and delte them. Insert each new value back into the table.
+ # model and delete them. Insert each new value back into the table.
collectionAttributes = _.filter attributes, (attr) ->
attr.queryable && attr instanceof AttributeCollection
@@ -204,7 +204,8 @@ class DatabaseTransaction
if joinedModels
for joined in joinedModels
joinMarks.push('(?,?)')
- joinedValues.push(model.id, joined.id)
+ joinValue = joined[attr.joinOnField ? "id"]
+ joinedValues.push(model.id, joinValue)
unless joinedValues.length is 0
# Write no more than 200 items (400 values) at once to avoid sqlite limits
diff --git a/src/flux/stores/focused-contacts-store.coffee b/src/flux/stores/focused-contacts-store.coffee
index f35bcb6cb..0b24ba353 100644
--- a/src/flux/stores/focused-contacts-store.coffee
+++ b/src/flux/stores/focused-contacts-store.coffee
@@ -4,6 +4,7 @@ Rx = require 'rx-lite'
Utils = require '../models/utils'
Actions = require '../actions'
NylasStore = require 'nylas-store'
+Thread = require '../models/thread'
Contact = require '../models/contact'
MessageStore = require './message-store'
AccountStore = require './account-store'
@@ -21,6 +22,8 @@ class FocusedContactsStore extends NylasStore
focusedContact: -> @_currentFocusedContact
+ focusedContactThreads: -> @_currentParticipantThreads ? []
+
# We need to wait now for the MessageStore to grab all of the
# appropriate messages for the given thread.
@@ -57,10 +60,12 @@ class FocusedContactsStore extends NylasStore
@_unsubFocusedContact = null
@_currentFocusedContact = null
@_currentThread = null
+ @_currentParticipantThreads = []
_onFocusContact: (contact) =>
@_unsubFocusedContact?.dispose()
@_unsubFocusedContact = null
+ @_currentParticipantThreads = []
if contact
query = DatabaseStore.findBy(Contact, {
@@ -70,10 +75,16 @@ class FocusedContactsStore extends NylasStore
@_unsubFocusedContact = Rx.Observable.fromQuery(query).subscribe (match) =>
@_currentFocusedContact = match ? contact
@trigger()
+ @_loadCurrentParticipantThreads(contact.email)
else
@_currentFocusedContact = null
@trigger()
+ _loadCurrentParticipantThreads: (email) ->
+ DatabaseStore.findAll(Thread).where(Thread.attributes.participants.contains(email)).limit(100).then (threads = []) =>
+ @_currentParticipantThreads = threads
+ @trigger()
+
# We score everyone to determine who's the most relevant to display in
# the sidebar.
_scoreAllParticipants: ->