diff --git a/dev-server/Phone.js b/dev-server/Phone.js index 7d9c52585a..cdf6e32b7b 100644 --- a/dev-server/Phone.js +++ b/dev-server/Phone.js @@ -6,11 +6,14 @@ import RcModule from 'ringcentral-integration/lib/RcModule'; import AccountExtension from 'ringcentral-integration/modules/AccountExtension'; import AccountInfo from 'ringcentral-integration/modules/AccountInfo'; +import AccountPhoneNumber from 'ringcentral-integration/modules/AccountPhoneNumber'; +import AddressBook from 'ringcentral-integration/modules/AddressBook'; import Alert from 'ringcentral-integration/modules/Alert'; import Auth from 'ringcentral-integration/modules/Auth'; import Brand from 'ringcentral-integration/modules/Brand'; import Call from 'ringcentral-integration/modules/Call'; import CallingSettings from 'ringcentral-integration/modules/CallingSettings'; +import Contacts from 'ringcentral-integration/modules/Contacts'; import ConnectivityMonitor from 'ringcentral-integration/modules/ConnectivityMonitor'; import DialingPlan from 'ringcentral-integration/modules/DialingPlan'; import ExtensionDevice from 'ringcentral-integration/modules/ExtensionDevice'; @@ -239,6 +242,12 @@ export default class Phone extends RcModule { tabManager: this.tabManager, getState: () => this.state.forwardingNumber, })); + this.addModule('contactMatcher', new ContactMatcher({ + ...options, + storage: this.storage, + getState: () => this.state.contactMatcher, + })); + reducers.contactMatcher = this.contactMatcher.reducer; this.addModule('webphone', new Webphone({ appKey: apiConfig.appKey, appName: 'RingCentral Widget', @@ -248,6 +257,7 @@ export default class Phone extends RcModule { client: this.client, storage: this.storage, rolesAndPermissions: this.rolesAndPermissions, + contactMatcher: this.contactMatcher, webphoneLogLevel: 3, extensionDevice: this.extensionDevice, getState: () => this.state.webphone, @@ -438,6 +448,9 @@ export default class Phone extends RcModule { contactMatcher: this.contactMatcher, webphone: this.webphone, onRinging: async () => { + if (this.webphone._webphone) { + return; + } // TODO refactor some of these logic into appropriate modules this.router.push('/calls'); }, @@ -454,12 +467,6 @@ export default class Phone extends RcModule { getState: () => this.state.callHistory, })); reducers.callHistory = this.callHistory.reducer; - this.addModule('contactMatcher', new ContactMatcher({ - ...options, - storage: this.storage, - getState: () => this.state.contactMatcher, - })); - reducers.contactMatcher = this.contactMatcher.reducer; this.addModule('activityMatcher', new ActivityMatcher({ ...options, storage: this.storage, @@ -477,6 +484,34 @@ export default class Phone extends RcModule { getState: () => this.state.callLogger, })); reducers.callLogger = this.callLogger.reducer; + this.addModule('accountPhoneNumber', new AccountPhoneNumber({ + auth: this.auth, + client: this.client, + storage: this.storage, + tabManager: this.tabManager, + getState: () => this.state.accountPhoneNumber, + })); + reducers.accountPhoneNumber = this.accountPhoneNumber.reducer; + this.addModule('addressBook', new AddressBook({ + client: this.client, + auth: this.auth, + storage: this.storage, + getState: () => this.state.addressBook, + })); + reducers.addressBook = this.addressBook.reducer; + this.addModule('contacts', new Contacts({ + client: this.client, + addressBook: this.addressBook, + accountPhoneNumber: this.accountPhoneNumber, + accountExtension: this.accountExtension, + getState: () => this.state.contacts, + })); + reducers.contacts = this.contacts.reducer; + this.contactMatcher.addSearchProvider({ + name: 'contacts', + searchFn: async ({ queries }) => this.contacts.matchContacts({ phoneNumbers: queries }), + readyCheckFn: () => this.contacts.ready, + }); this.addModule('conversationMatcher', new ConversationMatcher({ storage: this.storage, getState: () => this.state.conversationMatcher, diff --git a/dev-server/containers/App/index.js b/dev-server/containers/App/index.js index 24c64d3254..8266c20448 100644 --- a/dev-server/containers/App/index.js +++ b/dev-server/containers/App/index.js @@ -45,6 +45,13 @@ export default function App({ webphone={phone.webphone} regionSettings={phone.regionSettings} router={phone.router} + contactMatcher={phone.contactMatcher} + getAvatarUrl={ + async (contact) => { + const avatarUrl = await phone.contacts.getImageProfile(contact); + return avatarUrl; + } + } > ) : null; + let avatar; + if (props.avatarUrl) { + avatar = (avatar); + } else { + avatar = (); + } return (
- + {avatar}
- {props.name} - { timeCounter } + + {timeCounter}
{props.formatPhone(props.phoneNumber)} @@ -40,15 +61,23 @@ function CallInfo(props) { } CallInfo.propTypes = { - name: PropTypes.string.isRequired, phoneNumber: PropTypes.string, formatPhone: PropTypes.func.isRequired, startTime: PropTypes.number, + nameMatches: PropTypes.array.isRequired, + fallBackName: PropTypes.string.isRequired, + areaCode: PropTypes.string.isRequired, + countryCode: PropTypes.string.isRequired, + currentLocale: PropTypes.string.isRequired, + selectedMatcherIndex: PropTypes.number.isRequired, + onSelectMatcherName: PropTypes.func.isRequired, + avatarUrl: PropTypes.string, }; CallInfo.defaultProps = { phoneNumber: null, startTime: null, + avatarUrl: null, }; class ActiveCallPanel extends Component { @@ -74,10 +103,17 @@ class ActiveCallPanel extends Component { render() { const userInfo = this.state.isShowKeyPad ? null : ( ); const buttonsPad = this.state.isShowKeyPad ? null : ( @@ -130,7 +166,8 @@ class ActiveCallPanel extends Component { ActiveCallPanel.propTypes = { phoneNumber: PropTypes.string, - userName: PropTypes.string, + nameMatches: PropTypes.array.isRequired, + fallBackName: PropTypes.string.isRequired, currentLocale: PropTypes.string.isRequired, startTime: PropTypes.number, isOnMute: PropTypes.bool, @@ -148,6 +185,11 @@ ActiveCallPanel.propTypes = { onKeyPadChange: PropTypes.func.isRequired, formatPhone: PropTypes.func.isRequired, children: PropTypes.node, + areaCode: PropTypes.string.isRequired, + countryCode: PropTypes.string.isRequired, + selectedMatcherIndex: PropTypes.number.isRequired, + onSelectMatcherName: PropTypes.func.isRequired, + avatarUrl: PropTypes.string, }; ActiveCallPanel.defaultProps = { @@ -158,6 +200,7 @@ ActiveCallPanel.defaultProps = { isOnRecord: false, phoneNumber: null, children: undefined, + avatarUrl: null, }; export default ActiveCallPanel; diff --git a/src/components/ActiveCallPanel/styles.scss b/src/components/ActiveCallPanel/styles.scss index acf26548d5..257f11d065 100644 --- a/src/components/ActiveCallPanel/styles.scss +++ b/src/components/ActiveCallPanel/styles.scss @@ -11,6 +11,7 @@ $avatar-width: 60px; left: 0; z-index: 10; background: #ffffff; + text-align: center; } .backButton { @@ -34,12 +35,13 @@ $avatar-width: 60px; } .userInfo { + display: inline-block; text-align: left; color: $lightblack; margin-bottom: 8px; margin-top: 15px; - margin-left: 18%; - margin-right: 8%; + margin-left: auto; + margin-right: auto; } .avatarContainer { @@ -54,13 +56,18 @@ $avatar-width: 60px; border-radius: 50px; margin-left: auto; margin-right: auto; - opacity: 0.3; color: $primary-color; margin-bottom: 5px; + overflow: hidden; .icon { display: block; font-size: 38px; + opacity: 0.3; + } + + img { + width: 100%; } } @@ -79,6 +86,15 @@ $avatar-width: 60px; } } +.contactDisplay { + display: inline-block; + line-height: 16px; +} + +.contactNameSelect { + width: 100%; +} + .userPhoneNumber { font-size: 12px; color: $grey-light; diff --git a/src/components/ContactDisplay/index.js b/src/components/ContactDisplay/index.js index 947cd3db66..6a52488be5 100644 --- a/src/components/ContactDisplay/index.js +++ b/src/components/ContactDisplay/index.js @@ -12,6 +12,8 @@ const displayFomatter = ({ entityName, entityType, phoneNumber }) => { return `${entityName} | ${phoneSourceNames.getString(entityType)} ${phoneNumber}`; } else if (entityName && entityType) { return `${entityName} | ${phoneSourceNames.getString(entityType)}`; + } else if (entityName) { + return entityName; } else if (phoneNumber) { return `${phoneNumber}`; } @@ -32,6 +34,8 @@ export default function ContactDisplay({ phoneNumber, currentLocale, groupNumbers, + showType, + selectClassName, }) { let contentEl; if (groupNumbers) { @@ -74,7 +78,7 @@ export default function ContactDisplay({ ]; contentEl = ( ( displayFomatter({ entityName: options[value].name, - entityType: options[value].entityType, + entityType: showType && options[value].entityType, }) )} renderTitle={entity => ( @@ -130,6 +134,8 @@ ContactDisplay.propTypes = { phoneNumber: PropTypes.string, currentLocale: PropTypes.string.isRequired, groupNumbers: PropTypes.arrayOf(PropTypes.string), + showType: PropTypes.bool, + selectClassName: PropTypes.string, }; ContactDisplay.defaultProps = { className: undefined, @@ -138,4 +144,6 @@ ContactDisplay.defaultProps = { phoneNumber: undefined, groupNumbers: undefined, enableContactFallback: undefined, + showType: true, + selectClassName: undefined, }; diff --git a/src/components/IncomingCallPanel/index.js b/src/components/IncomingCallPanel/index.js index 8da0530ba1..11eb635aad 100644 --- a/src/components/IncomingCallPanel/index.js +++ b/src/components/IncomingCallPanel/index.js @@ -2,12 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import IncomingCallPad from '../IncomingCallPad'; - +import ContactDisplay from '../ContactDisplay'; import dynamicsFont from '../../assets/DynamicsFont/DynamicsFont.scss'; import styles from './styles.scss'; function UserInfo(props) { - const name = props.name; + let avatar; + if (props.avatarUrl) { + avatar = (avatar); + } else { + avatar = (); + } return (
@@ -15,11 +20,26 @@ function UserInfo(props) {
- + {avatar}
-
{name}
+
{props.formatPhone(props.phoneNumber)}
@@ -28,34 +48,39 @@ function UserInfo(props) { } UserInfo.propTypes = { - name: PropTypes.string.isRequired, phoneNumber: PropTypes.string, + currentLocale: PropTypes.string.isRequired, formatPhone: PropTypes.func.isRequired, + nameMatches: PropTypes.array.isRequired, + fallBackName: PropTypes.string.isRequired, + areaCode: PropTypes.string.isRequired, + countryCode: PropTypes.string.isRequired, + selectedMatcherIndex: PropTypes.number.isRequired, + onSelectMatcherName: PropTypes.func.isRequired, + avatarUrl: PropTypes.string, }; UserInfo.defaultProps = { className: null, phoneNumber: null, + avatarUrl: null, }; export default function IncomingCallPanel(props) { return (
-
-
-
- -
-
- )} + nameMatches={props.nameMatches} + fallBackName={props.fallBackName} + areaCode={props.areaCode} + countryCode={props.countryCode} + selectedMatcherIndex={props.selectedMatcherIndex} + onSelectMatcherName={props.onSelectMatcherName} + avatarUrl={props.avatarUrl} /> { + // `remember last matcher contact` will finish in next ticket + this.setState({ + selectedMatcherIndex: (index - 1), + avatarUrl: null, + }); + const nameMatches = this.props.session.direction === callDirections.outbound ? + this.props.toMatches : this.props.fromMatches; + const contact = nameMatches && nameMatches[index - 1]; + if (contact) { + this.props.getAvatarUrl(contact).then((avatarUrl) => { + this.setState({ avatarUrl }); + }); + } }; this.updatePositionOffset = (x, y) => { @@ -58,6 +75,20 @@ class ActiveCallPage extends Component { this.props.replyWithMessage(this.props.session.id, message); } + componentWillReceiveProps(nextProps) { + if (this.props.session.id !== nextProps.session.id) { + this.setState({ selectedMatcherIndex: 0 }); + const nameMatches = nextProps.session.direction === callDirections.outbound ? + nextProps.toMatches : nextProps.fromMatches; + const contact = nameMatches && nameMatches[0]; + if (contact) { + nextProps.getAvatarUrl(contact).then((avatarUrl) => { + this.setState({ avatarUrl }); + }); + } + } + } + render() { const session = this.props.session; const active = !!session.id; @@ -84,30 +115,33 @@ class ActiveCallPage extends Component { // isRinging = true; const phoneNumber = session.direction === callDirections.outbound ? session.to : session.from; - let userName; - if (session.direction === callDirections.inbound) { - userName = session.fromUserName; - if (session.from === 'anonymous') { - userName = i18n.getString('anonymous', this.props.currentLocale); - } - } else { - userName = session.toUserName; + const nameMatches = session.direction === callDirections.outbound ? + this.props.toMatches : this.props.fromMatches; + let fallbackUserName; + if (session.direction === callDirections.inbound && session.from === 'anonymous') { + fallbackUserName = i18n.getString('anonymous', this.props.currentLocale); } - if (!userName) { - userName = i18n.getString('unknown', this.props.currentLocale); + if (!fallbackUserName) { + fallbackUserName = i18n.getString('unknown', this.props.currentLocale); } if (isRinging) { return ( {this.props.children} @@ -118,7 +152,6 @@ class ActiveCallPage extends Component { currentLocale={this.props.currentLocale} formatPhone={this.props.formatPhone} phoneNumber={phoneNumber} - userName={userName} sessionId={session.id} callStatus={session.callStatus} startTime={session.startTime} @@ -135,6 +168,13 @@ class ActiveCallPage extends Component { onKeyPadChange={this.onKeyPadChange} hangup={this.hangup} onAdd={this.props.onAdd} + nameMatches={nameMatches} + fallBackName={fallbackUserName} + areaCode={this.props.areaCode} + countryCode={this.props.countryCode} + selectedMatcherIndex={this.state.selectedMatcherIndex} + onSelectMatcherName={this.onSelectMatcherName} + avatarUrl={this.state.avatarUrl} > {this.props.children} @@ -171,6 +211,11 @@ ActiveCallPage.propTypes = { formatPhone: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, children: PropTypes.node, + toMatches: PropTypes.array.isRequired, + fromMatches: PropTypes.array.isRequired, + areaCode: PropTypes.string.isRequired, + countryCode: PropTypes.string.isRequired, + getAvatarUrl: PropTypes.func.isRequired, }; ActiveCallPage.defaultProps = { @@ -180,12 +225,19 @@ ActiveCallPage.defaultProps = { function mapToProps(_, { webphone, locale, + contactMatcher, + regionSettings, }) { const currentSession = webphone.currentSession || {}; + const contactMapping = contactMatcher && contactMatcher.dataMapping; return { + fromMatches: (contactMapping && contactMapping[currentSession.from]) || [], + toMatches: (contactMapping && contactMapping[currentSession.to]) || [], currentLocale: locale.currentLocale, session: currentSession, minimized: webphone.minimized, + areaCode: regionSettings.areaCode, + countryCode: regionSettings.countryCode, }; } @@ -193,6 +245,7 @@ function mapToFunctions(_, { webphone, regionSettings, router, + getAvatarUrl, }) { return { formatPhone: phoneNumber => formatNumber({ @@ -217,6 +270,7 @@ function mapToFunctions(_, { toVoiceMail: sessionId => webphone.toVoiceMail(sessionId), replyWithMessage: (sessionId, message) => webphone.replyWithMessage(sessionId, message), toggleMinimized: () => webphone.toggleMinimized(), + getAvatarUrl, }; } @@ -229,6 +283,11 @@ ActiveCallContainer.propTypes = { webphone: PropTypes.instanceOf(Webphone).isRequired, locale: PropTypes.instanceOf(Locale).isRequired, regionSettings: PropTypes.instanceOf(RegionSettings).isRequired, + getAvatarUrl: PropTypes.func, +}; + +ActiveCallContainer.defaultProps = { + getAvatarUrl: () => null, }; export default ActiveCallContainer;