From 564ecca8e0da61d423314d30d2e6e638ac063756 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Fri, 20 Mar 2015 16:31:35 -0700 Subject: [PATCH] feat(sidebar): add more Salesforce states Summary: add more Salesforce states more salesforce sidebar extract focused contacts into its own store fullcontact store fixes extract thread participants to own module typo Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1326 --- exports/inbox-exports.coffee | 1 + internal_packages/message-list/lib/main.cjsx | 5 + .../lib/sidebar-thread-participants.cjsx | 49 ++++++++ .../stylesheets/message-list.less | 42 ++++++- .../lib/fullcontact-store.coffee | 112 ++---------------- .../lib/sidebar-fullcontact-chip.cjsx | 35 ------ .../lib/sidebar-fullcontact-details.cjsx | 57 ++++++++- .../lib/sidebar-fullcontact.cjsx | 26 ---- .../stylesheets/sidebar-fullcontact.less | 48 +++++--- keymaps/base.cson | 1 + .../stores/focused-contacts-store-spec.coffee | 24 ++++ src/flux/stores/focused-contacts-store.coffee | 109 +++++++++++++++++ static/images/sidebar/facebook-icon@2x.png | Bin 0 -> 622 bytes static/images/sidebar/icon-phone@2x.png | Bin 0 -> 799 bytes static/images/sidebar/linkedin-icon@2x.png | Bin 0 -> 637 bytes static/images/sidebar/twitter-icon@2x.png | Bin 0 -> 782 bytes 16 files changed, 323 insertions(+), 186 deletions(-) create mode 100644 internal_packages/message-list/lib/sidebar-thread-participants.cjsx delete mode 100644 internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx create mode 100644 spec-inbox/stores/focused-contacts-store-spec.coffee create mode 100644 src/flux/stores/focused-contacts-store.coffee create mode 100644 static/images/sidebar/facebook-icon@2x.png create mode 100644 static/images/sidebar/icon-phone@2x.png create mode 100644 static/images/sidebar/linkedin-icon@2x.png create mode 100644 static/images/sidebar/twitter-icon@2x.png diff --git a/exports/inbox-exports.coffee b/exports/inbox-exports.coffee index e50ea0f9c..fc524a9e5 100644 --- a/exports/inbox-exports.coffee +++ b/exports/inbox-exports.coffee @@ -46,6 +46,7 @@ module.exports = WorkspaceStore: require '../src/flux/stores/workspace-store' FileUploadStore: require '../src/flux/stores/file-upload-store' FileDownloadStore: require '../src/flux/stores/file-download-store' + FocusedContactsStore: require '../src/flux/stores/focused-contacts-store' ## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors SalesforceAssociation: require '../src/flux/models/salesforce-association' diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx index a3be90727..dcb4615ec 100644 --- a/internal_packages/message-list/lib/main.cjsx +++ b/internal_packages/message-list/lib/main.cjsx @@ -3,6 +3,7 @@ MessageList = require "./message-list" MessageToolbarItems = require "./message-toolbar-items" MessageSubjectItem = require "./message-subject-item" {ComponentRegistry, WorkspaceStore} = require 'inbox-exports' +SidebarThreadParticipants = require "./sidebar-thread-participants" {RetinaImg} = require 'ui-components' DownButton = React.createClass @@ -56,6 +57,10 @@ module.exports = view: UpButton location: WorkspaceStore.Sheet.Thread.Toolbar.Right + ComponentRegistry.register + name: 'SidebarThreadParticipants' + location: WorkspaceStore.Location.MessageListSidebar + view: SidebarThreadParticipants deactivate: -> ComponentRegistry.unregister 'MessageToolbarItems' diff --git a/internal_packages/message-list/lib/sidebar-thread-participants.cjsx b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx new file mode 100644 index 000000000..182700b20 --- /dev/null +++ b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx @@ -0,0 +1,49 @@ +_ = require 'underscore-plus' +React = require "react" + +{Actions, FocusedContactsStore} = require("inbox-exports") + +module.exports = +SidebarThreadParticipants = React.createClass + displayName: 'SidebarThreadParticipants' + + getInitialState: -> + sortedContacts: [] + focusedContact: null + + componentDidMount: -> + @unsubscribe = FocusedContactsStore.listen @_onChange + + componentWillUnmount: -> + @unsubscribe() + + render: -> +
+

Thread Participants

+ {@_renderSortedContacts()} +
+ + _renderSortedContacts: -> + contacts = [] + @state.sortedContacts.forEach (contact) => + if contact is @state.focusedContact + selected = "selected" + else selected = "" + contacts.push( +
@_onSelectContact(contact)} + key={contact.id}> + {contact.name} +
+ ) + return contacts + + _onSelectContact: (contact) -> + Actions.focusContact(contact) + + _onChange: -> + @setState(@_getStateFromStores()) + + _getStateFromStores: -> + sortedContacts: FocusedContactsStore.sortedContacts() + focusedContact: FocusedContactsStore.focusedContact() diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less index c4dcbdbd0..edea87ade 100644 --- a/internal_packages/message-list/stylesheets/message-list.less +++ b/internal_packages/message-list/stylesheets/message-list.less @@ -173,10 +173,6 @@ } -.column-MessageListSidebar { - background-color: @background-off-primary; -} - /////////////////////////////// // message-participants.cjsx // /////////////////////////////// @@ -235,3 +231,41 @@ } } } + +/////////////////////////////// +// sidebar-thread-participants.cjsx // +/////////////////////////////// +.sidebar-thread-participants { + padding: @spacing-standard; + order: 10; + flex-shrink: 0; + + .other-contact { + color: @text-color-subtle; + font-size: @font-size-smaller; + &.selected { + font-weight: @font-weight-semi-bold; + } + &:hover {cursor: pointer;} + } + + // TODO: DRY + h2.sidebar-h2 { + font-size: 11px; + font-weight: @font-weight-semi-bold; + text-transform: uppercase; + color: @text-color-very-subtle; + border-bottom: 1px solid @border-color-divider; + margin: 2em 0 1em 0; + } +} + +.column-MessageListSidebar { + background-color: @background-off-primary; + overflow: auto; + border-left: 1px solid #ddd; + .flexbox-handle-horizontal div { + border-right: 0; + width: 1px; + } +} diff --git a/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee b/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee index d9982ff5a..4c5f5be2b 100644 --- a/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee +++ b/internal_packages/sidebar-fullcontact/lib/fullcontact-store.coffee @@ -1,125 +1,32 @@ _ = require 'underscore-plus' Reflux = require 'reflux' request = require 'request' - -{Utils, - Actions, - MessageStore, - NamespaceStore} = require 'inbox-exports' +{FocusedContactsStore} = require 'inbox-exports' module.exports = FullContactStore = Reflux.createStore init: -> - # @listenTo Actions.getFullContactDetails, @_makeDataRequest - @listenTo Actions.selectThreadId, @_onSelectThreadId - @listenTo Actions.focusContact, @_focusContact - @listenTo MessageStore, @_onMessageStoreChanged - @listenTo NamespaceStore, @_onNamespaceChanged - @_cachedContactData = {} + @listenTo FocusedContactsStore, @_onFocusedContacts - @_currentThreadId = null - @_clearCurrentParticipants(silent: true) - - @_onNamespaceChanged() - - sortedContacts: -> @_currentContacts - - focusedContact: -> @_currentFocusedContact + sortedContacts: -> FocusedContactsStore.sortedContacts() + focusedContact: -> FocusedContactsStore.focusedContact() fullContactCache: -> emails = {} - emails[contact.email] = contact for contact in @_currentContacts + contacts = FocusedContactsStore.sortedContacts() + emails[contact.email] = contact for contact in contacts fullContactCache = {} _.each @_cachedContactData, (fullContactData, email) -> if email of emails then fullContactCache[email] = fullContactData return fullContactCache - _clearCurrentParticipants: ({silent}={}) -> - @_contactScores = {} - @_currentContacts = [] - @_currentFocusedContact = null - @trigger() unless silent - - _onSelectThreadId: (id) -> - @_currentThreadId = id - @_clearCurrentParticipants() - # We need to wait now for the MessageStore to grab all of the - # appropriate messages for the given thread. - - _onMessageStoreChanged: -> - if MessageStore.threadId() is @_currentThreadId - @_setCurrentParticipants() - else - @_clearCurrentParticipants() - - _onNamespaceChanged: -> - @_myEmail = (NamespaceStore.current()?.me().email ? "").toLowerCase().trim() - - # For now we take the last message - _setCurrentParticipants: -> - @_scoreAllParticipants() - sorted = _.sortBy(_.values(@_contactScores), "score").reverse() - @_currentContacts = _.map(sorted, (obj) -> obj.contact) - @_focusContact(@_currentContacts[0], silent: true) - @trigger() - - _focusContact: (contact, {silent}={}) -> - return unless contact - @_currentFocusedContact = contact + _onFocusedContacts: -> + contact = FocusedContactsStore.focusedContact() ? {} if not @_cachedContactData[contact.email] @_fetchAPIData(contact.email) - @trigger() unless silent - - # We score everyone to determine who's the most relevant to display in - # the sidebar. - _scoreAllParticipants: -> - score = (message, msgNum, field, multiplier) => - for contact, j in (message[field] ? []) - bonus = message[field].length - j - @_assignScore(contact, (msgNum+1) * multiplier + bonus) - - for message, msgNum in MessageStore.items() by -1 - if message.draft - score(message, msgNum, "to", 10000) - score(message, msgNum, "cc", 1000) - else - score(message, msgNum, "from", 100) - score(message, msgNum, "to", 10) - score(message, msgNum, "cc", 1) - return @_contactScores - - # Self always gets a score of 0 - _assignScore: (contact, score=0) -> - return unless contact?.email - return if contact.email.trim().length is 0 - return if @_contactScores[contact.nameEmail()]? # only assign the first time - - penalties = @_calculatePenalties(contact, score) - - @_contactScores[contact.nameEmail()] = - contact: contact - score: score - penalties - - _calculatePenalties: (contact, score) -> - penalties = 0 - email = contact.email.toLowerCase().trim() - - if email is @_myEmail - penalties += score # The whole thing which will penalize to zero - - notCommonDomain = not Utils.emailHasCommonDomain(@_myEmail) - sameDomain = Utils.emailsHaveSameDomain(@_myEmail, email) - if notCommonDomain and sameDomain - penalties += score * 0.9 - - return Math.max(penalties, 0) - - _matchesDomain: (myEmail, email) -> - myDomain = _.last(myEmail.split("@")) - theirDomain = _.last(email.split("@")) - return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain + @trigger() _fetchAPIData: (email) -> # Swap the url's to see real data @@ -130,6 +37,5 @@ FullContactStore = Reflux.createStore return {} if resp.statusCode != 200 try data = JSON.parse data - console.log data @_cachedContactData[email] = data @trigger(@) diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx deleted file mode 100644 index e3f8528c9..000000000 --- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-chip.cjsx +++ /dev/null @@ -1,35 +0,0 @@ -_ = require 'underscore-plus' -React = require "react" - -{Actions} = require 'inbox-exports' - -module.exports = -SidebarFullContactChip = React.createClass - - render: -> -
- { - for contact in @props.contacts - if contact.name != contact.email - @_makeContactChip(contact, @props.compact) - } - { - for contact in @props.contacts - if contact.name == contact.email - @_makeContactChip(contact, @props.compact) - } -
- - _makeContactChip: (contact, compact) -> - if contact.name == contact.email or compact == true -
@props.selectContact(contact.email)} > -
{contact.email}
-
- else -
@props.selectContact(contact.email)} > - { - if compact != true -

{contact.name}

- } -
{contact.email}
-
diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx index f0ecb0e16..92e49cafd 100644 --- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx +++ b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.cjsx @@ -2,10 +2,16 @@ _ = require 'underscore-plus' React = require "react" {Actions} = require 'inbox-exports' +{RetinaImg} = require 'ui-components' module.exports = SidebarFullContactDetails = React.createClass + _supportedProfileTypes: + twitter: true + linkedin: true + facebook: true + propTypes: contact: React.PropTypes.object fullContact: React.PropTypes.object @@ -21,12 +27,55 @@ SidebarFullContactDetails = React.createClass
{@_title()}
{@_company()}
- {@_renderActions()} +
+ {@_socialProfiles()} +
+ {@_noInfo()} - _renderActions: -> -
-
+ _socialProfiles: -> + profiles = @_profiles() + return profiles.map (profile) => +
+ +
+ {@_username(profile)} + {@_twitterBio(profile)} +
+
+ + _profiles: -> + profiles = @props.fullContact.socialProfiles ? [] + profiles = _.filter profiles, (p) => @_supportedProfileTypes[p.typeId] + + _showSocialProfiles: -> + @_profiles().length > 0 + + _username: (profile) -> + if (profile.username ? "").length > 0 + if profile.typeId is "twitter" + return "@#{profile.username}" + else + return profile.username + else + return profile.typeName + + _noInfo: -> + if not @_showSocialProfiles() and not @_showSubheader() +
No additional information available.
+ else return "" + + _twitterBio: (profile) -> + return "" unless profile.typeId is "twitter" + return "" unless profile.bio?.length > 0 + + # http://stackoverflow.com/a/13398311/793472 + twitterRegex = /(^|[^@\w])@(\w{1,15})\b/g + replace = '$1@$2' + bio = profile.bio.replace(twitterRegex, replace) +
_showSubheader: -> @_title().length > 0 or @_company().length > 0 diff --git a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx index 2b09dcc11..834c99bc2 100644 --- a/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx +++ b/internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.cjsx @@ -4,14 +4,11 @@ FullContactStore = require "./fullcontact-store" SidebarFullContactDetails = require "./sidebar-fullcontact-details.cjsx" -{Actions} = require("inbox-exports") - module.exports = SidebarFullContact = React.createClass getInitialState: -> fullContactCache: {} - sortedContacts: [] focusedContact: null componentDidMount: -> @@ -24,30 +21,8 @@ SidebarFullContact = React.createClass
-
-

Thread Participants

- {@_renderSortedContacts()} -
- _renderSortedContacts: -> - contacts = [] - @state.sortedContacts.forEach (contact) => - if contact is @state.focusedContact - selected = "selected" - else selected = "" - contacts.push( -
@_onSelectContact(contact)} - key={contact.id}> - {contact.name} -
- ) - return contacts - - _onSelectContact: (contact) -> - Actions.focusContact(contact) - _fullContact: -> if @state.focusedContact?.email return @state.fullContactCache[@state.focusedContact.email] ? {} @@ -59,7 +34,6 @@ SidebarFullContact = React.createClass _getStateFromStores: -> fullContactCache: FullContactStore.fullContactCache() - sortedContacts: FullContactStore.sortedContacts() focusedContact: FullContactStore.focusedContact() SidebarFullContact.maxWidth = 300 diff --git a/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less b/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less index b1c53874a..835784d3b 100644 --- a/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less +++ b/internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less @@ -1,6 +1,8 @@ .full-contact-sidebar { padding: @spacing-standard; + padding-bottom: 0; order: 1; + flex-shrink: 0; .full-contact { h1.name { @@ -24,31 +26,49 @@ } .subheader { - border-top: 1px solid @border-color-divider; - margin: @spacing-standard 0; color: @text-color-subtle; - padding: @spacing-standard 0; + padding: 0 0 @spacing-standard 0; font-size: @font-size-smaller; } } + .social-profiles { + border-top: 1px solid @border-color-divider; + padding-top: 7px; + } + .social-profile { + margin-top: 0.5em; + .social-icon { + padding-top: 6px; + float: left; + } + .social-link { + padding-left: @spacing-double; + font-size: @font-size-smaller; + a { + text-decoration: none; + } + } + } + + h2.sidebar-h2 { - font-size: 10px; + font-size: 11px; font-weight: @font-weight-semi-bold; text-transform: uppercase; color: @text-color-very-subtle; border-bottom: 1px solid @border-color-divider; margin: 2em 0 1em 0; } - - .other-contacts { - .other-contact { - color: @text-color-subtle; - font-size: @font-size-smaller; - &.selected { - font-weight: @font-weight-semi-bold; - } - &:hover {cursor: pointer;} - } + .sidebar-extra-info { + font-size: 10px; + font-weight: @font-weight-medium; + color: @text-color-subtle; } + .sidebar-no-info { + font-size: @font-size-smaller; + color: fade(@text-color, 30%); + } + + } diff --git a/keymaps/base.cson b/keymaps/base.cson index c0b63d9f4..74726d85c 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -24,6 +24,7 @@ 'f' : 'application:forward' # Gmail 'escape': 'application:pop-sheet' + 'u': 'application:pop-sheet' # Gmail # Default cross-platform core behaviors 'left': 'core:move-left' diff --git a/spec-inbox/stores/focused-contacts-store-spec.coffee b/spec-inbox/stores/focused-contacts-store-spec.coffee new file mode 100644 index 000000000..bfa36295c --- /dev/null +++ b/spec-inbox/stores/focused-contacts-store-spec.coffee @@ -0,0 +1,24 @@ +proxyquire = require 'proxyquire' +Reflux = require 'reflux' + +MessageStoreStub = Reflux.createStore + items: -> [] + threadId: -> null + +NamespaceStoreStub = Reflux.createStore + current: -> null + +FocusedContactsStore = proxyquire '../../src/flux/stores/focused-contacts-store', + "./message-store": MessageStoreStub + "./namespace-store": NamespaceStoreStub + +describe "FocusedContactsStore", -> + beforeEach -> + FocusedContactsStore._currentThreadId = null + FocusedContactsStore._clearCurrentParticipants(silent: true) + + it "returns no contacts with empty", -> + expect(FocusedContactsStore.sortedContacts()).toEqual [] + + it "returns no focused contact when empty", -> + expect(FocusedContactsStore.focusedContact()).toBeNull() diff --git a/src/flux/stores/focused-contacts-store.coffee b/src/flux/stores/focused-contacts-store.coffee new file mode 100644 index 000000000..bac2245a8 --- /dev/null +++ b/src/flux/stores/focused-contacts-store.coffee @@ -0,0 +1,109 @@ +_ = require 'underscore-plus' +Reflux = require 'reflux' + +Utils = require '../models/utils.coffee' +Actions = require '../actions' +MessageStore = require './message-store' +NamespaceStore = require './namespace-store' + +# A store that handles the focuses collections of and individual contacts +module.exports = +FocusedContactsStore = Reflux.createStore + init: -> + @listenTo Actions.selectThreadId, @_onSelectThreadId + @listenTo Actions.focusContact, @_focusContact + @listenTo MessageStore, => @_onMessageStoreChanged() + @listenTo NamespaceStore, => @_onNamespaceChanged() + + @_currentThreadId = null + @_clearCurrentParticipants(silent: true) + + @_onNamespaceChanged() + + sortedContacts: -> @_currentContacts + + focusedContact: -> @_currentFocusedContact + + _clearCurrentParticipants: ({silent}={}) -> + @_contactScores = {} + @_currentContacts = [] + @_currentFocusedContact = null + @trigger() unless silent + + _onSelectThreadId: (id) -> + @_currentThreadId = id + @_clearCurrentParticipants() + # We need to wait now for the MessageStore to grab all of the + # appropriate messages for the given thread. + + _onMessageStoreChanged: -> + if MessageStore.threadId() is @_currentThreadId + @_setCurrentParticipants() + else + @_clearCurrentParticipants() + + _onNamespaceChanged: -> + @_myEmail = (NamespaceStore.current()?.me().email ? "").toLowerCase().trim() + + # For now we take the last message + _setCurrentParticipants: -> + @_scoreAllParticipants() + sorted = _.sortBy(_.values(@_contactScores), "score").reverse() + @_currentContacts = _.map(sorted, (obj) -> obj.contact) + @_focusContact(@_currentContacts[0], silent: true) + @trigger() + + _focusContact: (contact, {silent}={}) -> + return unless contact + @_currentFocusedContact = contact + @trigger() unless silent + + # We score everyone to determine who's the most relevant to display in + # the sidebar. + _scoreAllParticipants: -> + score = (message, msgNum, field, multiplier) => + for contact, j in (message[field] ? []) + bonus = message[field].length - j + @_assignScore(contact, (msgNum+1) * multiplier + bonus) + + for message, msgNum in MessageStore.items() by -1 + if message.draft + score(message, msgNum, "to", 10000) + score(message, msgNum, "cc", 1000) + else + score(message, msgNum, "from", 100) + score(message, msgNum, "to", 10) + score(message, msgNum, "cc", 1) + return @_contactScores + + # Self always gets a score of 0 + _assignScore: (contact, score=0) -> + return unless contact?.email + return if contact.email.trim().length is 0 + return if @_contactScores[contact.nameEmail()]? # only assign the first time + + penalties = @_calculatePenalties(contact, score) + + @_contactScores[contact.nameEmail()] = + contact: contact + score: score - penalties + + _calculatePenalties: (contact, score) -> + penalties = 0 + email = contact.email.toLowerCase().trim() + + if email is @_myEmail + penalties += score # The whole thing which will penalize to zero + + notCommonDomain = not Utils.emailHasCommonDomain(@_myEmail) + sameDomain = Utils.emailsHaveSameDomain(@_myEmail, email) + if notCommonDomain and sameDomain + penalties += score * 0.9 + + return Math.max(penalties, 0) + + _matchesDomain: (myEmail, email) -> + myDomain = _.last(myEmail.split("@")) + theirDomain = _.last(email.split("@")) + return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain + diff --git a/static/images/sidebar/facebook-icon@2x.png b/static/images/sidebar/facebook-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd27de6cb3dc3f36cdbc6bed9eb7abe74dc1cd4 GIT binary patch literal 622 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpv0K~pAc6dEh{T4CMG5> zF0Q1c1Y|&fsHiBAAt@;-BO@a(FE0%SKoOuaMMXt*b#)*ENNQ?oN=ZopMHCbifZBnA zK=nW_P$iHdA|e7LB_t$(<^V}KIXR#)KnBo8H8nM$UZ4p;#Xv3uV5)>T07wE21Ud<# z0B9r7E+7|ZI8X`*G&D4T9$8&I=Q5CTE(!7rW-tu&mJSKG2sd*!F)*^!D$&S^4(uU#g)5k*xx}su$=15EGT+2E_^(Vf3){YD@dwJ<7n}}YJY81BQhU5i^#J2@=O)4D z$x|5ZW}Yzkd&q~$?7NtER(SUL)Wq_fh=ka-=%%~&iSm5x7Fw_8-TN%5_sjg>)CbSH cU;e9LI`T@ax`~OC7wC2dPgg&ebxsLQ0Nw$fHvj+t literal 0 HcmV?d00001 diff --git a/static/images/sidebar/icon-phone@2x.png b/static/images/sidebar/icon-phone@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..da816e4c82ff9de6658172cfa7a101fc282019af GIT binary patch literal 799 zcmV+)1K|9LP)Px%*-1n}R5%f(R8341K@^^u-EB*|fg&1cLnC5Sg*L@N2>!%_2O$AtR3s#7;(>&d z@o2<&F`m3|@?iXn8Z;rAcn~~bFeMsI3>E13=TUIV}X(QhvVkGOTuEr6frgAm%$Vrn2&xFv0{k zn%NkW#)ji9rGV=kbAGcpRfG#O5Cvy}^9q`ssw_!DY{p9!QXa`_hKF3U0>wVrG*C>4qJ~y#sr|${` zTbeHuVdt0`E%CSp9eMd}2qP;4;8g7Q1%jjFBSUQdIgevQcq$fK+v~5I5(J@{34>vg zhz+}TSN47xAD)BQEY5bznqX(0jS((w+PE-7l5o?$R#sJ)5KO9d3UqI=N+-M(74M^; zM}7jv1u*!w*7NPx0{d!?6nk7_@mP#~51!u`g{}h+np|Ca?J6xhfH5xn17cYcE$95f z8moP~GyE+cXT>6jUKNPY0Zkl|9R=;s&^C|D6P=idu0lS^-fTAMV10dInY+9P23>1R z!d`!9s?rHlS{4NHCgjZZE2^@7vAgTuBjYHzhBh-fsT4WoXR@;>2%UR*0#sx|grkIz zrVOHtCQ*!{k>QU6`UT;IhIKs@3e64ly=YcdaDA@RC@!bMM-RhU*ciF z@)!YTKSMR`VgKOU8;M9{4Im|3VOG!00uI^X_lvc@fO~df_Sc90{^eAXiT-t-c~Pz_ d`$id?`wfKA)=7{J5nccQ002ovPDHLkV1gMXWoiHb literal 0 HcmV?d00001 diff --git a/static/images/sidebar/linkedin-icon@2x.png b/static/images/sidebar/linkedin-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..539e8ab5afca2b92b5e4c5aca3f3241aa42dff75 GIT binary patch literal 637 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAv7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpv2(-pAc6dJ=B3i!D
A((Z#x<+_87>-~aJ5U-YnkdRBYm?#;!IZGiv=E= zuWHph-u_(kh|O`i^p~pI@63z_3U&*46^*3!Co%;XO+9@xW{NA*w0)9p2ecToKhEGi zAgmyNxpHg#1o!R#8Ch*Ev6UrVYka}`yF>oO#A2;y!AH*fYpkrl;JTuXyXe&3O4oV! gW{7g{j{nGT=DfUJ^mFwBptl%2UHx3vIVCg!0QESK8UO$Q literal 0 HcmV?d00001 diff --git a/static/images/sidebar/twitter-icon@2x.png b/static/images/sidebar/twitter-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5df62ca215b77a9257f341af1dcd4d34b9a009ea GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^GC(ZJ!3-ot<@SjJDVB6cUq=Rpjs4tz5?O(Kg=CK) zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a!@$7E8sHP+3RDoAoIH{d zFa$)76q^F6u#zCZUc1DeYzW&|}S!N@qR95Gnz=iIh+L^k;M!QVyYm_=ozH)0jPk*)5S4F<9zO=^yotd0t^p=x9l}q z8W0n*Qll?RR%6G+hYm;I{{OF?=24VeEdT4tyIQ@oOddzi|MQY-l|e&xRJ0p%Xij#>5G}0oD|w- zN6Qr7+Mp`7TX*u@ySDZ!JM^Df6@(}%Rh)SH;s*=wMX}!H89d$(B9C{7?)bx%;v@Y5M!GD@)lvZa0*fD1Yzxi-ot< zc5{UM{h0aZ&!b6}uh%chp0xF45ZA}=Y!7e$W@rEY`L(rU(dxq|&*$4k{$*Z!QPz4% Sxgsd_GI+ZBxvX