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 ( +
+
+
{abv} +
+
+
+ ) + } + + _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: ->