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 = ;
+ }
+
+ return (
+
+ );
+ }
+}
+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 = ;
- }
-
- return (
-
- );
- }
-
- _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 });