diff --git a/app/internal_packages/participant-profile/assets/crunchbase-sidebar-icon@2x.png b/app/internal_packages/participant-profile/assets/crunchbase-sidebar-icon@2x.png new file mode 100644 index 000000000..f94d96280 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/crunchbase-sidebar-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/employees-icon@2x.png b/app/internal_packages/participant-profile/assets/employees-icon@2x.png new file mode 100644 index 000000000..273050b35 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/employees-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/funding-icon@2x.png b/app/internal_packages/participant-profile/assets/funding-icon@2x.png new file mode 100644 index 000000000..8e1acc910 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/funding-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/holding-icon@2x.png b/app/internal_packages/participant-profile/assets/holding-icon@2x.png new file mode 100644 index 000000000..6f85229c4 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/holding-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/industry-icon@2x.png b/app/internal_packages/participant-profile/assets/industry-icon@2x.png new file mode 100644 index 000000000..1dbfa5974 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/industry-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/location-icon@2x.png b/app/internal_packages/participant-profile/assets/location-icon@2x.png index 662cc3561..6bb8d0b92 100644 Binary files a/app/internal_packages/participant-profile/assets/location-icon@2x.png and b/app/internal_packages/participant-profile/assets/location-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/phone-icon@2x.png b/app/internal_packages/participant-profile/assets/phone-icon@2x.png new file mode 100644 index 000000000..2f28d12d2 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/phone-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/assets/timezone-icon@2x.png b/app/internal_packages/participant-profile/assets/timezone-icon@2x.png new file mode 100644 index 000000000..e24afd258 Binary files /dev/null and b/app/internal_packages/participant-profile/assets/timezone-icon@2x.png differ diff --git a/app/internal_packages/participant-profile/lib/participant-profile-data-source.es6 b/app/internal_packages/participant-profile/lib/participant-profile-data-source.es6 index 0c20067f8..5eda30099 100644 --- a/app/internal_packages/participant-profile/lib/participant-profile-data-source.es6 +++ b/app/internal_packages/participant-profile/lib/participant-profile-data-source.es6 @@ -2,8 +2,8 @@ import { MailspringAPIRequest, Utils } from 'mailspring-exports'; const { makeRequest } = MailspringAPIRequest; const CACHE_SIZE = 200; -const CACHE_INDEX_KEY = 'pp-cache-keys'; -const CACHE_KEY_PREFIX = 'pp-cache-'; +const CACHE_INDEX_KEY = 'pp-cache-v3-keys'; +const CACHE_KEY_PREFIX = 'pp-cache-v3-'; class ParticipantProfileDataSource { constructor() { @@ -37,56 +37,15 @@ class ParticipantProfileDataSource { return {}; } - let person = (body || {}).person; - - // This means there was no data about the person available. Return a - // valid, but empty object for us to cache. This can happen when we - // have company data, but no personal data. - if (!person) { - person = { email }; + if (!body.person) { + body.person = { email }; + } + if (!body.company) { + body.company = {}; } - const result = { - cacheDate: Date.now(), - email: email, // Used as checksum - bio: - person.bio || - (person.twitter && person.twitter.bio) || - (person.aboutme && person.aboutme.bio), - location: person.location || (person.geo && person.geo.city) || null, - currentTitle: person.employment && person.employment.title, - currentEmployer: person.employment && person.employment.name, - profilePhotoUrl: person.avatar, - rawClearbitData: body, - socialProfiles: this._socialProfiles(person), - }; - - this.setCache(email, result); - return result; - } - - _socialProfiles(person = {}) { - const profiles = {}; - - if (((person.twitter && person.twitter.handle) || '').length > 0) { - profiles.twitter = { - handle: person.twitter.handle, - url: `https://twitter.com/${person.twitter.handle}`, - }; - } - if (((person.facebook && person.facebook.handle) || '').length > 0) { - profiles.facebook = { - handle: person.facebook.handle, - url: `https://facebook.com/${person.facebook.handle}`, - }; - } - if (((person.linkedin && person.linkedin.handle) || '').length > 0) { - profiles.linkedin = { - handle: person.linkedin.handle, - url: `https://linkedin.com/${person.linkedin.handle}`, - }; - } - return profiles; + this.setCache(email, body); + return body; } // LocalStorage Retrieval / Saving diff --git a/app/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx b/app/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx index 59068029a..34f2445ef 100644 --- a/app/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx +++ b/app/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx @@ -8,22 +8,171 @@ import { Utils, } from 'mailspring-exports'; import { RetinaImg } from 'mailspring-component-kit'; +import moment from 'moment-timezone'; import ParticipantProfileDataSource from './participant-profile-data-source'; -/* We expect ParticipantProfileDataSource.find 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 - * } - */ +class ProfilePictureOrColorBox extends React.Component { + static propTypes = { + loading: PropTypes.bool, + contact: PropTypes.object, + profilePicture: PropTypes.string, + }; + render() { + const { contact, loading, avatar } = this.props; + + const hue = Utils.hueForString(contact.email); + const bgColor = `hsl(${hue}, 50%, 45%)`; + + let content = ( +
+ {contact.nameAbbreviation()} +
+ ); + + if (loading) { + content = ( +
+ +
+ ); + } + + if (avatar) { + content = Profile; + } + + return ( +
+
{content}
+
+ ); + } +} +class SocialProfileLink extends React.Component { + static propTypes = { + service: PropTypes.string, + handle: PropTypes.string, + }; + + render() { + const { handle, service } = this.props; + + if (!handle) { + return false; + } + return ( + + + + ); + } +} + +class TextBlockWithAutolinkedElements extends React.Component { + static propTypes = { + className: PropTypes.string, + string: PropTypes.string, + }; + + render() { + if (!this.props.string) { + return false; + } + + const nodes = []; + const hashtagOrMentionRegex = RegExpUtils.hashtagOrMentionRegex(); + + let remainder = this.props.string; + let match = null; + let count = 0; + + /* I thought we were friends. */ + /* eslint no-cond-assign: 0 */ + while ((match = hashtagOrMentionRegex.exec(remainder))) { + // the first char of the match is whitespace, match[1] is # or @, match[2] is the tag itself. + nodes.push(remainder.substr(0, match.index + 1)); + if (match[1] === '#') { + nodes.push( + {`#${match[2]}`} + ); + } + if (match[1] === '@') { + nodes.push({`@${match[2]}`}); + } + remainder = remainder.substr(match.index + match[0].length); + count += 1; + } + nodes.push(remainder); + + return

{nodes}

; + } +} + +class IconRow extends React.Component { + static propTypes = { + string: PropTypes.string, + icon: PropTypes.string, + }; + + render() { + const { string, icon } = this.props; + + if (!string) { + return false; + } + return ( +
+ + + {string} + +
+ ); + } +} + +class LocationRow extends React.Component { + static propTypes = { + string: PropTypes.string, + }; + + render() { + return ( + + {this.props.string} + {' ['} + + View + + {']'} + + ) + } + /> + ); + } +} export default class SidebarParticipantProfile extends React.Component { static displayName = 'SidebarParticipantProfile'; @@ -94,156 +243,7 @@ export default class SidebarParticipantProfile extends React.Component { }); }; - _renderProfilePhoto() { - const hue = Utils.hueForString(this.props.contact.email); - const bgColor = `hsl(${hue}, 50%, 45%)`; - - let content = ( -
- {this.props.contact.nameAbbreviation()} -
- ); - - if (this.state.loading) { - content = ( -
- -
- ); - } - - if (this.state.profilePhotoUrl) { - content = Profile; - } - - return ( -
-
{content}
-
- ); - } - - _renderCorePersonalInfo() { - const fullName = this.props.contact.fullName(); - let renderName = false; - if (fullName !== this.props.contact.email) { - renderName = ( -
- {this.props.contact.fullName()} -
- ); - } - return ( -
- {renderName} -
- {this.props.contact.email} -
- {this._renderSocialProfiles()} -
- ); - } - - _renderSocialProfiles() { - if (!this.state.socialProfiles) { - return false; - } - const profiles = Object.entries(this.state.socialProfiles).map(([type, profile]) => { - return ( - - - - ); - }); - return
{profiles}
; - } - - _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; - } - - const bioNodes = []; - const hashtagOrMentionRegex = RegExpUtils.hashtagOrMentionRegex(); - - let bioRemainder = this.state.bio; - let match = null; - let count = 0; - - /* I thought we were friends. */ - /* eslint no-cond-assign: 0 */ - while ((match = hashtagOrMentionRegex.exec(bioRemainder))) { - // the first char of the match is whitespace, match[1] is # or @, match[2] is the tag itself. - bioNodes.push(bioRemainder.substr(0, match.index + 1)); - if (match[1] === '#') { - bioNodes.push( - {`#${match[2]}`} - ); - } - if (match[1] === '@') { - bioNodes.push({`@${match[2]}`}); - } - bioRemainder = bioRemainder.substr(match.index + match[0].length); - count += 1; - } - bioNodes.push(bioRemainder); - - return

{bioNodes}

; - } - - _renderLocation() { - if (!this.state.location) { - return false; - } - return ( -

- - - {this.state.location} - -

- ); - } - - _select(event) { + _onSelect = event => { const el = event.target; const sel = document.getSelection(); if (el.contains(sel.anchorNode) && !sel.isCollapsed) { @@ -254,7 +254,7 @@ export default class SidebarParticipantProfile extends React.Component { if (anchor && focus && focus.data) { sel.setBaseAndExtent(anchor, 0, focus, focus.data.length); } - } + }; _renderFindCTA() { if (!this.state.trialing || this.state.loaded) { @@ -277,12 +277,150 @@ export default class SidebarParticipantProfile extends React.Component { ); } - render() { + _renderCompanyInfo() { + const { + name, + domain, + category, + description, + location, + timeZone, + logo, + facebook, + twitter, + linkedin, + crunchbase, + type, + ticker, + phone, + metrics, + } = + this.state.company || {}; + + if (!name) { + return; + } + + let employees = null; + let funding = null; + + if (metrics) { + if (metrics.raised) { + funding = `Raised $${(metrics.raised / 1 || 0).toLocaleString()}`; + } else if (metrics.marketCap) { + funding = `Market cap $${(metrics.marketCap / 1 || 0).toLocaleString()}`; + } + + if (metrics.employees) { + employees = `${(metrics.employees / 1 || 0).toLocaleString()} employees`; + } else if (metrics.employeesRange) { + employees = `${metrics.employeesRange} employees`; + } + } + + return ( +
+ {logo && ( + + )} + +
+ {name} +
+ + {domain && ( + + {domain} + + )} + +
+ + + + {`${timeZone.replace('_', ' ')} - `} + + {`Currently ${moment() + .tz(timeZone) + .format('h:MMa')}`} + + + ) + } + /> + + + + + + +
+ + + + +
+
+
+ ); + } + + _renderPersonInfo() { + const { facebook, linkedin, twitter, employment, location, bio } = this.state.person || {}; + return (
- {this._renderProfilePhoto()} - {this._renderCorePersonalInfo()} - {this._renderAdditionalInfo()} + +
+ {this.props.contact.fullName() !== this.props.contact.email && ( +
+ {this.props.contact.fullName()} +
+ )} + + {employment && ( +
+ {employment.title && {employment.title}, } + {employment.name} +
+ )} + +
+ {this.props.contact.email} +
+ +
+ + + +
+
+ +
+ + +
+
+ ); + } + + render() { + return ( +
+ {this._renderPersonInfo()} + + {this._renderCompanyInfo()} {this._renderFindCTA()}
diff --git a/app/internal_packages/participant-profile/styles/participant-profile.less b/app/internal_packages/participant-profile/styles/participant-profile.less index 81117cb13..dd826d3bb 100644 --- a/app/internal_packages/participant-profile/styles/participant-profile.less +++ b/app/internal_packages/participant-profile/styles/participant-profile.less @@ -45,9 +45,25 @@ } } -.participant-profile { +.company-profile { + padding-top: 20px; + border-top: 1px solid @border-color-divider; + .company-logo { + margin-left: 12px; + float: right; + max-width: 60px; + max-height: 44px; + } +} + +.participant-profile, +.company-profile { margin-bottom: 22px; + .larger { + font-size: 16px; + } + .profile-photo-wrap { width: 50px; height: 50px; @@ -87,7 +103,20 @@ } } - .core-personal-info { + .social-profiles-wrap { + margin-bottom: @spacing-standard; + } + .social-profile-item { + margin: 0 10px; + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + + .personal-info { padding-top: 30px; text-align: center; margin-bottom: @spacing-standard; @@ -99,24 +128,24 @@ white-space: nowrap; } - .full-name { - font-size: 16px; - } .email { color: @text-color-very-subtle; margin-bottom: @spacing-standard; } - .social-profiles-wrap { - margin-bottom: @spacing-standard; - } - .social-profile-item { - margin: 0 10px; - } } + .additional-info { + margin-top: 10px; font-size: 12px; + clear: both; p { - margin-bottom: 15px; + margin-bottom: 16px; + } + .icon-row { + margin-bottom: 8px; + a.plain { + color: @text-color; + } } .bio { color: @text-color-very-subtle; @@ -125,7 +154,8 @@ } body.platform-win32 { - .participant-profile { + .participant-profile, + .company-profile { border-radius: 0; .profile-photo { border-radius: 0; diff --git a/app/src/flux/stores/draft-editing-session.es6 b/app/src/flux/stores/draft-editing-session.es6 index 726c05866..3a387de5c 100644 --- a/app/src/flux/stores/draft-editing-session.es6 +++ b/app/src/flux/stores/draft-editing-session.es6 @@ -81,7 +81,9 @@ class DraftChangeSet extends EventEmitter { this._saving = this._pending; this._pending = {}; + console.log('_saving = ' + JSON.stringify(this._saving)); return this.callbacks.onCommit().then(() => { + console.log('_saving cleared'); this._saving = {}; }); }; @@ -347,6 +349,8 @@ export default class DraftEditingSession extends MailspringStore { return; } + console.log('_onDraftChanged'); + // If our draft has been changed, only accept values which are present. // If `body` is undefined, assume it's not loaded. Do not overwrite old body. const nextDraft = change.objects @@ -364,6 +368,7 @@ export default class DraftEditingSession extends MailspringStore { } nextValues[key] = nextDraft[key]; } + console.log('_setDraft nextValues: ' + JSON.stringify(nextValues)); this._setDraft(Object.assign(new Message(), this._draft, nextValues)); this.trigger(); } @@ -391,8 +396,12 @@ export default class DraftEditingSession extends MailspringStore { const baseDraft = draft || inMemoryDraft; const updatedDraft = this.changes.applyToModel(baseDraft); const task = new SyncbackDraftTask({ draft: updatedDraft }); + console.log('changeSetCommit queueing task'); Actions.queueTask(task); await TaskQueue.waitForPerformLocal(task); + console.log( + 'changeSetCommit finished waiting for performLocal. At this point, onDraftChanged should have been called.' + ); } // Undo / Redo diff --git a/app/src/flux/stores/draft-store.es6 b/app/src/flux/stores/draft-store.es6 index d91786c68..b5fa5e948 100644 --- a/app/src/flux/stores/draft-store.es6 +++ b/app/src/flux/stores/draft-store.es6 @@ -355,8 +355,12 @@ class DraftStore extends MailspringStore { // completely saved and the user won't see old content briefly. const session = await this.sessionForClientId(headerMessageId); await session.ensureCorrectAccount(); - await session.changes.commit(); let draft = session.draft(); + console.log('1:'); + console.log(JSON.stringify(draft)); + await session.changes.commit(); + console.log('2:'); + console.log(JSON.stringify(session.draft())); await session.teardown(); draft = await DraftHelpers.applyExtensionTransforms(draft); @@ -369,6 +373,9 @@ class DraftStore extends MailspringStore { // the new message text (and never old draft text or blank text) sending. await MessageBodyProcessor.updateCacheForMessage(draft); + console.log('3:'); + console.log(JSON.stringify(draft)); + // At this point the message UI enters the sending state and the composer is unmounted. this.trigger({ headerMessageId });