diff --git a/app/components/AudioCallBox.js b/app/components/AudioCallBox.js index 9495277..c02d40b 100644 --- a/app/components/AudioCallBox.js +++ b/app/components/AudioCallBox.js @@ -1,231 +1,232 @@ import React, { Component } from 'react'; import { View, Platform } from 'react-native'; import { IconButton, Dialog, Text } from 'react-native-paper'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import Logger from "../../Logger"; import CallOverlay from './CallOverlay'; import DTMFModal from './DTMFModal'; import EscalateConferenceModal from './EscalateConferenceModal'; import UserIcon from './UserIcon'; import utils from '../utils'; import styles from '../assets/styles/blink/_AudioCallBox.scss'; const logger = new Logger("AudioCallBox"); class AudioCallBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { active : false, audioMuted : false, showDtmfModal : false, showEscalateConferenceModal : false }; // this.speechEvents = null; this.remoteAudio = React.createRef(); } componentDidMount() { // This component is used both for as 'local media' and as the in-call component. // Thus, if the call is not null it means we are beyond the 'local media' phase // so don't call the mediaPlaying prop. if (this.props.call != null) { switch (this.props.call.state) { case 'established': this.attachStream(this.props.call); break; case 'incoming': this.props.mediaPlaying(); // fall through default: this.props.call.on('stateChanged', this.callStateChanged); break; } } else { this.props.mediaPlaying(); } } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.call == null && nextProps.call) { if (nextProps.call.state === 'established') { this.attachStream(nextProps.call); } else { nextProps.call.on('stateChanged', this.callStateChanged); } } } componentWillUnmount() { clearTimeout(this.callTimer); // if (this.speechEvents !== null) { // this.speechEvents.stop(); // this.speechEvents = null; // } } callStateChanged(oldState, newState, data) { if (newState === 'established') { this.attachStream(this.props.call); } } attachStream(call) { this.setState({stream: call.getRemoteStreams()[0]}); //we dont use it anywhere though as audio gets automatically piped // const options = { // interval: 225, // play: false // }; // this.speechEvents = hark(remoteStream, options); // this.speechEvents.on('speaking', () => { // this.setState({active: true}); // }); // this.speechEvents.on('stopped_speaking', () => { // this.setState({active: false}); // }); } escalateToConference(participants) { this.props.escalateToConference(participants); } hangupCall(event) { event.preventDefault(); this.props.hangupCall(); } muteAudio(event) { event.preventDefault(); //const localStream = this.props.call.getLocalStreams()[0]; if(this.state.audioMuted) { logger.debug('Unmute microphone'); this.props.callKeepToggleMute(false); //localStream.getAudioTracks()[0].enabled = true; this.setState({audioMuted: false}); } else { logger.debug('Mute microphone'); //localStream.getAudioTracks()[0].enabled = false; this.props.callKeepToggleMute(true); this.setState({audioMuted: true}); } } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } toggleEscalateConferenceModal() { this.setState({ showEscalateConferenceModal: !this.state.showEscalateConferenceModal }); } render() { let remoteIdentity; if (this.props.call !== null) { remoteIdentity = this.props.call.remoteIdentity; } else { remoteIdentity = {uri: this.props.remoteUri}; } const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; let displayName = (this.props.remoteDisplayName && this.props.remoteUri !== this.props.remoteDisplayName) ? this.props.remoteDisplayName: this.props.remoteUri; return ( {displayName} { (this.props.remoteDisplayName && this.props.remoteUri !== this.props.remoteDisplayName) ? {this.props.remoteUri} : null } ); } } AudioCallBox.propTypes = { remoteUri : PropTypes.string.isRequired, call : PropTypes.object, remoteDisplayName : PropTypes.string, escalateToConference : PropTypes.func, hangupCall : PropTypes.func, mediaPlaying : PropTypes.func, callKeepSendDtmf : PropTypes.func, callKeepToggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool }; export default AudioCallBox; diff --git a/app/components/Call.js b/app/components/Call.js index cbed758..a7ad526 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,226 +1,227 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import assert from 'assert'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import Logger from "../../Logger"; import AudioCallBox from './AudioCallBox'; import LocalMedia from './LocalMedia'; import VideoBox from './VideoBox'; import config from '../config'; const logger = new Logger("Call"); class Call extends Component { constructor(props) { super(props); autoBind(this); if (this.props.localMedia && this.props.localMedia.getVideoTracks().length === 0) { //logger.debug('Will send audio only'); this.state = {audioOnly: true}; } else { this.state = {audioOnly: false}; } // If current call is available on mount we must have incoming if (this.props.currentCall != null) { this.props.currentCall.on('stateChanged', this.callStateChanged); } } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { // Needed for switching to incoming call while in a call if (this.props.currentCall != null && this.props.currentCall != nextProps.currentCall) { if (nextProps.currentCall != null) { nextProps.currentCall.on('stateChanged', this.callStateChanged); } else { this.props.currentCall.removeListener('stateChanged', this.callStateChanged); } } } callStateChanged(oldState, newState, data) { // console.log('Call: callStateChanged', newState, '->', newState); if (newState === 'established') { // Check the media type again, remote can choose to not accept all offered media types const currentCall = this.props.currentCall; const remoteHasStreams = currentCall.getRemoteStreams().length > 0; const remoteHasNoVideoTracks = currentCall.getRemoteStreams()[0].getVideoTracks().length === 0; const remoteIsRecvOnly = currentCall.remoteMediaDirections.video[0] === 'recvonly'; const remoteIsInactive = currentCall.remoteMediaDirections.video[0] === 'inactive'; if (remoteHasStreams && (remoteHasNoVideoTracks || remoteIsRecvOnly || remoteIsInactive) && !this.state.audioOnly) { console.log('Media type changed to audio'); // Stop local video if (this.props.localMedia.getVideoTracks().length !== 0) { currentCall.getLocalStreams()[0].getVideoTracks()[0].stop(); } this.setState({audioOnly: true}); this.props.speakerphoneOff(); } else { this.forceUpdate(); } currentCall.removeListener('stateChanged', this.callStateChanged); // Switch to video earlier. The callOverlay has a handle on // 'established'. It starts a timer. To prevent a state updating on // unmounted component we try to switch on 'accept'. This means we get // to localMedia first. } else if (newState === 'accepted') { // Switch if we have audioOnly and local videotracks. This means // the call object switched and we are transitioning to an // incoming call. if (this.state.audioOnly && this.props.localMedia && this.props.localMedia.getVideoTracks().length !== 0) { console.log('Media type changed to video on accepted'); this.setState({audioOnly: false}); this.props.speakerphoneOn(); } } this.forceUpdate(); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } call() { assert(this.props.currentCall === null, 'currentCall is not null'); let options = {pcConfig: {iceServers: config.iceServers}, id: this.props.callUUID}; options.localStream = this.props.localMedia; let call = this.props.account.call(this.props.targetUri, options); call.on('stateChanged', this.callStateChanged); } answerCall() { assert(this.props.currentCall !== null, 'currentCall is null'); let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = this.props.localMedia; this.props.currentCall.answer(options); } hangupCall() { let callUUID = this.props.currentCall._callkeepUUID; this.props.currentCall.removeListener('stateChanged', this.callStateChanged); this.props.hangupCall(callUUID); } mediaPlaying() { if (this.props.currentCall === null) { this.call(); } else { this.answerCall(); } } render() { //console.log('Call: render call to', this.props.targetUri); let box = null; let remoteUri = this.props.targetUri; let remoteDisplayName = remoteUri; if (this.props.currentCall !== null && this.props.currentCall.state == 'established') { remoteUri = this.props.currentCall.remoteIdentity.uri; remoteDisplayName = this.props.currentCall.remoteIdentity.displayName || this.props.currentCall.remoteIdentity.uri; } else { remoteUri = this.props.targetUri; remoteDisplayName = this.props.targetUri; } if (remoteUri.indexOf('3333@') > -1) { remoteDisplayName = 'Video Test'; } else if (remoteUri.indexOf('4444@') > -1) { remoteDisplayName = 'Echo Test'; } else { if (this.props.contacts) { var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', remoteUri); if (contact_obj) { remoteDisplayName = contact_obj.displayName; } } } if (this.props.localMedia !== null) { if (this.state.audioOnly) { box = ( ); } else { if (this.props.currentCall != null && this.props.currentCall.state === 'established') { box = ( ); } else { //console.log('Will render local media'); if (this.props.currentCall && this.props.currentCall.state && this.props.currentCall.state === 'terminated') { // do not render } else { box = ( ); } } } } return box; } } Call.propTypes = { targetUri : PropTypes.string.isRequired, account : PropTypes.object.isRequired, hangupCall : PropTypes.func.isRequired, localMedia : PropTypes.object, currentCall : PropTypes.object, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, callKeepToggleMute : PropTypes.func, speakerphoneOn : PropTypes.func, speakerphoneOff : PropTypes.func, callUUID : PropTypes.string, contacts : PropTypes.array }; export default Call; diff --git a/app/components/CallByUriBox.js b/app/components/CallByUriBox.js index aeb8e46..11062bb 100644 --- a/app/components/CallByUriBox.js +++ b/app/components/CallByUriBox.js @@ -1,103 +1,104 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Title, Button, TextInput } from 'react-native-paper'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import Call from './Call'; class CallByUriBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { displayName: '' }; this._notificationCenter = null; } componentDidMount() { this._notificationCenter = this.props.notificationCenter(); } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!this.props.currentCall && nextProps.currentCall) { nextProps.currentCall.on('stateChanged', this.callStateChanged); } } callStateChanged(oldState, newState, data) { if (newState === 'terminated') { this._notificationCenter.postSystemNotification('Thanks for calling with Sylk!', {timeout: 10}); } } handleDisplayNameChange(event) { this.setState({displayName: event.target.value}); } handleSubmit(event) { event.preventDefault(); this.props.handleCallByUri(this.state.displayName, this.props.targetUri); } render() { const validInput = this.state.displayName !== ''; let content; if (this.props.localMedia !== null) { content = ( ); } else { content = ( You've been invited to call {this.props.targetUri} ); } return ( {content} ); } } CallByUriBox.propTypes = { handleCallByUri : PropTypes.func.isRequired, notificationCenter : PropTypes.func.isRequired, hangupCall : PropTypes.func.isRequired, shareScreen : PropTypes.func.isRequired, targetUri : PropTypes.string, localMedia : PropTypes.object, account : PropTypes.object, currentCall : PropTypes.object, generatedVideoTrack : PropTypes.bool }; export default CallByUriBox; diff --git a/app/components/CallOverlay.js b/app/components/CallOverlay.js index b7112ba..789243c 100644 --- a/app/components/CallOverlay.js +++ b/app/components/CallOverlay.js @@ -1,121 +1,122 @@ import React from 'react'; import { View, Text } from 'react-native'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import autoBind from 'auto-bind'; import { Appbar } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; class CallOverlay extends React.Component { constructor(props) { super(props); autoBind(this); this.duration = null; this.timer = null; this._isMounted = true; } componentDidMount() { if (this.props.call) { if (this.props.call.state === 'established') { this.startTimer(); } else if (this.props.call.state !== 'terminated') { this.props.call.on('stateChanged', this.callStateChanged); } } } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.call == null && nextProps.call) { if (nextProps.call.state === 'established') { this.startTimer(); } else if (nextProps.call.state !== 'terminated') { nextProps.call.on('stateChanged', this.callStateChanged); } } } componentWillUnmount() { this._isMounted = false; clearTimeout(this.timer); } callStateChanged(oldState, newState, data) { // Prevent starting timer when we are unmounted if (newState === 'established' && this._isMounted) { this.startTimer(); this.props.call.removeListener('stateChanged', this.callStateChanged); } } startTimer() { if (this.timer !== null) { // already armed return; } // TODO: consider using window.requestAnimationFrame const startTime = new Date(); this.timer = setInterval(() => { this.duration = moment.duration(new Date() - startTime).format('hh:mm:ss', {trim: false}); if (this.props.show) { this.forceUpdate(); } }, 300); } render() { let header = null; let displayName = this.props.remoteUri; if (this.props.remoteDisplayName && this.props.remoteDisplayName !== this.props.remoteUri) { displayName = this.props.remoteDisplayName; } if (this.props.show) { let callDetail; if (this.duration !== null) { callDetail = {this.duration}; callDetail = 'Duration:' + this.duration; } else { callDetail = 'Connecting...' } if (this.props.remoteUri.search('videoconference') > -1) { header = ( ); } else { header = ( ); } } return header } } CallOverlay.propTypes = { show: PropTypes.bool.isRequired, remoteUri: PropTypes.string.isRequired, remoteDisplayName: PropTypes.string, call: PropTypes.object }; export default CallOverlay; diff --git a/app/components/ConferenceByUriBox.js b/app/components/ConferenceByUriBox.js index a8e375b..e8de716 100644 --- a/app/components/ConferenceByUriBox.js +++ b/app/components/ConferenceByUriBox.js @@ -1,122 +1,123 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Title, Button, TextInput } from 'react-native-paper'; import Conference from './Conference'; class ConferenceByUriBox extends React.Component { constructor(props) { super(props); autoBind(this); this.state = { displayName: '' }; this._notificationCenter = null; } componentDidMount() { this._notificationCenter = this.props.notificationCenter(); } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { if (!this.props.currentCall && nextProps.currentCall) { nextProps.currentCall.on('stateChanged', this.callStateChanged); } } callStateChanged(oldState, newState, data) { if (newState === 'terminated') { this._notificationCenter.postSystemNotification('Thanks for calling with Sylk!', {timeout: 10}); } } handleDisplayNameChange(event) { this.setState({displayName: event.target.value}); } handleSubmit(event) { event.preventDefault(); let displayName; if (this.state.displayName === '') { this.setState({displayName: 'Guest'}); displayName = 'Guest'; } else { displayName = this.state.displayName; // Bug in SIPSIMPLE, display name can't end with \ else we don't join chat if (displayName.endsWith('\\')) { displayName = displayName.slice(0, -1); } } this.props.handler(displayName, this.props.targetUri); } render() { let content; if (this.props.localMedia !== null) { content = ( ); } else { const classes = classNames({ 'capitalize' : true, 'btn' : true, 'btn-lg' : true, 'btn-block' : true, 'btn-primary': true }); const friendlyName = this.props.targetUri.split('@')[0]; content = ( You're about to join a conference! {friendlyName} ); } return ( {content} ); } } ConferenceByUriBox.propTypes = { notificationCenter : PropTypes.func.isRequired, handler : PropTypes.func.isRequired, hangupCall : PropTypes.func.isRequired, shareScreen : PropTypes.func.isRequired, targetUri : PropTypes.string, localMedia : PropTypes.object, account : PropTypes.object, currentCall : PropTypes.object, generatedVideoTrack : PropTypes.bool }; export default ConferenceByUriBox; diff --git a/app/components/ConferenceModal.js b/app/components/ConferenceModal.js index 926fa65..9e28426 100644 --- a/app/components/ConferenceModal.js +++ b/app/components/ConferenceModal.js @@ -1,97 +1,98 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; import { Portal, Dialog, Button, Text, TextInput, Surface, Chip } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import config from '../config'; import styles from '../assets/styles/blink/_ConferenceModal.scss'; class ConferenceModal extends Component { constructor(props) { super(props); this.state = { conferenceTargetUri: props.targetUri.split('@')[0], managed: false }; this.handleConferenceTargetChange = this.handleConferenceTargetChange.bind(this); this.onHide = this.onHide.bind(this); this.joinAudio = this.joinAudio.bind(this); this.joinVideo = this.joinVideo.bind(this); } - componentWillReceiveProps(nextProps) { + //getDerivedStateFromProps(nextProps, state) { + UNSAFE_componentWillReceiveProps(nextProps) { this.setState({conferenceTargetUri: nextProps.targetUri.split('@')[0]}); } handleConferenceTargetChange(value) { this.setState({conferenceTargetUri: value}); } joinAudio(event) { event.preventDefault(); const uri = `${this.state.conferenceTargetUri.replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: false}); } joinVideo(event) { event.preventDefault(); const uri = `${this.state.conferenceTargetUri.replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: true}); } onHide() { this.props.handleConferenceCall(null); } render() { const validUri = this.state.conferenceTargetUri.length > 0 && this.state.conferenceTargetUri.indexOf('@') === -1; return ( Join Conference ); } } ConferenceModal.propTypes = { show: PropTypes.bool.isRequired, handleConferenceCall: PropTypes.func.isRequired, targetUri: PropTypes.string.isRequired }; export default ConferenceModal; diff --git a/app/components/HistoryTileBox.js b/app/components/HistoryTileBox.js index f0da246..b254dd3 100644 --- a/app/components/HistoryTileBox.js +++ b/app/components/HistoryTileBox.js @@ -1,236 +1,238 @@ import React, { Component} from 'react'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import { SafeAreaView, ScrollView, View, FlatList, Text } from 'react-native'; import HistoryCard from './HistoryCard'; import utils from '../utils'; import DigestAuthRequest from 'digest-auth-request'; import storage from '../storage'; import uuid from 'react-native-uuid'; import styles from '../assets/styles/blink/_HistoryTileBox.scss'; class HistoryTileBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { serverHistory: [], refreshHistory: this.props.refreshHistory } const echoTest = { remoteParty: '4444@sylk.link', displayName: 'Echo test', type: 'contact', label: 'Call to test microphone', id: uuid.v4() }; const videoTest = { remoteParty: '3333@sylk.link', displayName: 'Video test', type: 'contact', label: 'Call to test video', id: uuid.v4() }; const echoTestCard = Object.assign({}, echoTest); const videoTestCard = Object.assign({}, videoTest); let initialContacts = [echoTestCard, videoTestCard]; this.initialContacts = initialContacts; storage.get('history').then((history) => { if (history) { this.setState({localHistory: history}); } }); } componentDidMount() { this.getServerHistory(); } refreshHistory = res => this.setState({ serverHistory: res.history}) renderItem(item) { return( ); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } + + //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(props) { if (props.refreshHistory !== this.state.refreshHistory) { this.getServerHistory(); } } getServerHistory() { let history = []; utils.timestampedLog('Requesting call history from server'); let getServerCallHistory = new DigestAuthRequest( 'GET', `${this.props.config.serverCallHistoryUrl}?action=get_history&realm=${this.props.account.id.split('@')[1]}`, this.props.account.id.split('@')[0], this.props.password ); // Disable logging getServerCallHistory.loggingOn = false; getServerCallHistory.request((data) => { if (data.success !== undefined && data.success === false) { logger.debug('Error getting call history from server: %o', data.error_message) return; } if (data.placed) { data.placed.map(elem => {elem.direction = 'placed'; return elem}); history = history.concat(data.placed); } if (data.received) { data.received.map(elem => {elem.direction = 'received'; return elem}); history = history.concat(data.received); } if (history) { history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) const known = []; history = history.filter((elem) => { if (known.indexOf(elem.remoteParty) <= -1) { elem.type = 'history'; var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', elem.remoteParty); if (contact_obj) { elem.displayName = contact_obj.name; elem.photo = contact_obj.photo; // TODO update icon here } else { elem.photo = null; } elem.label = elem.direction; if (!elem.displayName) { elem.displayName = elem.remoteParty; } if (!elem.media || !Array.isArray(elem.media)) { elem.media = ['audio']; } if (elem.remoteParty.indexOf('@videoconference') > -1) { elem.remoteParty = elem.remoteParty.split('@')[0] + '@videoconference.' + this.props.config.defaultDomain; } if ((elem.media.indexOf('audio') > -1 || elem.media.indexOf('video') > -1) && (elem.remoteParty !== this.props.account.id || elem.direction !== 'placed')) { known.push(elem.remoteParty); if (elem.remoteParty.indexOf('3333@') > -1) { // see Call.js as well if we change this elem.displayName = 'Video Test'; } if (elem.remoteParty.indexOf('4444@') > -1) { // see Call.js as well if we change this elem.displayName = 'Echo Test'; } elem.id = uuid.v4(); return elem; } } }); if (history.length < 3) { history = history.concat(this.initialContacts); } this.setState({serverHistory: history}); } }, (errorCode) => { logger.debug('Error getting call history from server: %o', errorCode) }); } render() { //utils.timestampedLog('Render history'); // Join URIs from local and server history for input let matchedContacts = []; let items = this.state.serverHistory.filter(historyItem => historyItem.remoteParty.startsWith(this.props.targetUri)); let searchExtraItems = this.props.contacts; searchExtraItems.concat(this.initialContacts); if (this.props.targetUri && this.props.targetUri.length > 2 && !this.props.selectedContact) { matchedContacts = searchExtraItems.filter(contact => (contact.remoteParty.toLowerCase().search(this.props.targetUri) > -1 || contact.displayName.toLowerCase().search(this.props.targetUri) > -1)); } else if (this.props.selectedContact && this.props.selectedContact.type === 'contact') { matchedContacts.push(this.props.selectedContact); } items = items.concat(matchedContacts); //console.log(items); items = items.slice(0, 8); //utils.timestampedLog('Render history in', this.props.orientation); let columns = 1; if (this.props.isTablet) { columns = this.props.orientation === 'landscape' ? 3 : 2; } else { columns = this.props.orientation === 'landscape' ? 2 : 1; } return ( item.id} key={this.props.orientation} /> ); } } HistoryTileBox.propTypes = { account : PropTypes.object.isRequired, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, targetUri : PropTypes.string, selectedContact : PropTypes.object, contacts : PropTypes.array, orientation : PropTypes.string, setTargetUri : PropTypes.func, isTablet : PropTypes.bool, refreshHistory : PropTypes.bool }; export default HistoryTileBox;