diff --git a/app/components/HistoryCard.js b/app/components/HistoryCard.js index 049cf2b..4072e7b 100644 --- a/app/components/HistoryCard.js +++ b/app/components/HistoryCard.js @@ -1,310 +1,311 @@ import React, { Component} from 'react'; import { View, SafeAreaView, FlatList } from 'react-native'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import { Card, IconButton, Button, Caption, Title, Subheading, List, Text} from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import uuid from 'react-native-uuid'; import styles from '../assets/styles/blink/_HistoryCard.scss'; import UserIcon from './UserIcon'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } const Item = ({ nr, uri, displayName }) => ( {displayName !== uri? {nr}. {displayName} ({uri}) : {nr}. {uri} } ); const renderItem = ({ item }) => ( ); class HistoryCard extends Component { constructor(props) { super(props); autoBind(this); this.state = { id: this.props.contact.id, displayName: this.props.contact.displayName, uri: this.props.contact.remoteParty, participants: this.props.contact.participants, conference: this.props.contact.conference, type: this.props.contact.type, photo: this.props.contact.photo, label: this.props.contact.label, orientation: this.props.orientation, isTablet: this.props.isTablet, favorite: (this.props.contact.tags.indexOf('favorite') > -1)? true : false, blocked: (this.props.contact.tags.indexOf('blocked') > -1)? true : false } } shouldComponentUpdate(nextProps) { //https://medium.com/sanjagh/how-to-optimize-your-react-native-flatlist-946490c8c49b return true; } handleParticipant() { } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } setBlockedUri() { let newBlockedState = this.props.setBlockedUri(this.state.uri); this.setState({blocked: newBlockedState}); } deleteHistoryEntry() { this.props.deleteHistoryEntry(this.state.uri); } setFavoriteUri() { let newFavoriteState = this.props.setFavoriteUri(this.state.uri); this.setState({favorite: newFavoriteState}); } setTargetUri(uri, contact) { if (this.isAnonymous(this.state.uri)) { return; } this.props.setTargetUri(this.state.uri, this.props.contact); } isAnonymous(uri) { if (uri.search('@guest.') > -1 || uri.search('@anonymous.') > -1) { return true } return false; } render () { let containerClass = styles.portraitContainer; let cardClass = styles.card; //console.log('Render card', this.state.uri, this.state.orientation); let showActions = this.props.contact.showActions && this.props.contact.tags.indexOf('test') === -1; let uri = this.state.uri; let displayName = this.state.displayName; let buttonMode = 'text'; let showBlockButton = uri.search('@videoconference.') === -1 ? true : false; let showFavoriteButton = true; let showDeleteButton = this.props.contact.tags.indexOf('local') > -1 ? true: false; let blockTextbutton = 'Block'; let favoriteTextbutton = 'Favorite'; let deleteTextbutton = 'Delete'; let participantsData = []; if (this.isAnonymous(uri)) { uri = 'anonymous@anonymous.invalid'; displayName = displayName + ' - from the Web'; let showFavoriteButton = false; } if (this.state.favorite) { favoriteTextbutton = 'Remove favorite'; if (!this.state.blocked) { showBlockButton = false; } } if (uri.search('3333@') === -1) { showBlockButton = false; } if (uri.search('4444@') === -1) { showBlockButton = false; } if (displayName === 'Myself') { showBlockButton = false; } if (this.state.blocked) { blockTextbutton = 'Unblock'; showFavoriteButton = false; } if (this.state.isTablet) { containerClass = (this.state.orientation === 'landscape') ? styles.landscapeTabletContainer : styles.portraitTabletContainer; } else { containerClass = (this.state.orientation === 'landscape') ? styles.landscapeContainer : styles.portraitContainer; } if (showActions) { cardClass = styles.expandedCard; } let color = {}; let title = displayName || uri.split('@')[0]; let subtitle = uri; let description = this.props.contact.startTime; if (displayName === uri) { title = toTitleCase(uri.split('@')[0]); } if (this.state.type === 'history') { let duration = moment.duration(this.props.contact.duration, 'seconds').format('hh:mm:ss', {trim: false}); if (this.props.contact.direction === 'received' && this.props.contact.duration === 0) { color.color = '#a94442'; duration = 'missed'; } else if (this.props.contact.direction === 'placed' && this.props.contact.duration === 0) { duration = 'cancelled'; } if (this.state.conference) { if (this.state.participants && this.state.participants.length) { if (!showActions) { subtitle = 'With ' + this.state.participants.length + ' participants'; } else { let i = 1; let contact_obj; let dn; + let _item; this.state.participants.forEach((participant) => { contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', participant); dn = contact_obj ? contact_obj.displayName : participant; - let _item = {nr: i, id: uuid.v4(), uri: participant, displayName: dn}; + _item = {nr: i, id: uuid.v4(), uri: participant, displayName: dn}; participantsData.push(_item); i = i + 1; }); } } else { subtitle = 'No participants'; } } if (!displayName) { title = uri; if (duration === 'missed') { subtitle = 'Last call missed'; } else if (duration === 'cancelled') { subtitle = 'Last call cancelled'; } else { subtitle = 'Last call duration ' + duration ; } } description = description + ' (' + duration + ')'; return ( {this.setTargetUri(uri, this.props.contact)}} style={[containerClass, cardClass]} > {title} {subtitle} {description} {this.state.participants && this.state.participants.length ? Participants: item.id} /> : null} {showActions ? {showDeleteButton? : null} {showBlockButton? : null} {showFavoriteButton?: null} : null} ); } else { return ( {this.props.setTargetUri(uri, this.props.contact)}} style={[containerClass, cardClass]} > {title} {uri} {this.state.label} {showActions ? {showBlockButton? : null} {showFavoriteButton?: null} : null} ); } } } HistoryCard.propTypes = { id : PropTypes.string, contact : PropTypes.object, setTargetUri : PropTypes.func, setBlockedUri : PropTypes.func, setFavoriteUri : PropTypes.func, deleteHistoryEntry : PropTypes.func, orientation : PropTypes.string, isTablet : PropTypes.bool, contacts : PropTypes.array }; export default HistoryCard; diff --git a/app/components/HistoryTileBox.js b/app/components/HistoryTileBox.js index 8b012fb..153a1d4 100644 --- a/app/components/HistoryTileBox.js +++ b/app/components/HistoryTileBox.js @@ -1,376 +1,425 @@ 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 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: this.props.serverHistory, localHistory: this.props.localHistory, accountId: this.props.account.id, password: this.props.password, targetUri: this.props.targetUri, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris } const echoTest = { remoteParty: '4444@sylk.link', displayName: 'Echo test', type: 'contact', label: 'Call to test microphone', id: uuid.v4(), tags: ['test'] }; this.echoTest = Object.assign({}, echoTest); const videoTest = { remoteParty: '3333@sylk.link', displayName: 'Video test', type: 'contact', label: 'Call to test video', id: uuid.v4(), tags: ['test'] }; this.videoTest = Object.assign({}, videoTest); } componentDidMount() { this.getServerHistory(); } setTargetUri(uri, contact) { //console.log('Set target uri uri in history list', uri); this.props.setTargetUri(uri, contact); } deleteHistoryEntry(uri) { this.props.deleteHistoryEntry(uri); this.props.setTargetUri(uri); } setFavoriteUri(uri) { return this.props.setFavoriteUri(uri); + this.props.setTargetUri(); } setBlockedUri(uri) { return this.props.setBlockedUri(uri); } 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) { const { refreshHistory } = this.props; if (props.refreshHistory !== refreshHistory) { this.getServerHistory(); } } getLocalHistory() { let history = this.state.localHistory; history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) let known = []; history = history.filter((elem) => { if (known.indexOf(elem.remoteParty) <= -1) { elem.type = 'history'; if (!elem.tags) { elem.tags = []; } if (elem.tags.indexOf('history') === -1) { elem.tags.push('history'); } if (elem.tags.indexOf('local') === -1) { elem.tags.push('local'); } known.push(elem.remoteParty); return elem; } }); return history; } + getFavoriteContacts() { + let favoriteContacts = []; + let contact_obj; + let displayName; + let label; + let media; + let conference; + + this.state.favoriteUris.forEach((uri) => { + contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', uri); + displayName = contact_obj ? contact_obj.displayName : uri; + label = contact_obj ? contact_obj.label: null; + media = ['audio']; + conference = false; + + if (uri.indexOf('@videoconference.') > -1) { + displayName = 'Conference ' + uri.split('@')[0]; + uri = uri.split('@')[0] + '@' + this.props.config.defaultConferenceDomain; + conference = true; + media = ['audio', 'video', 'chat']; + } + + const item = { + remoteParty: uri, + displayName: displayName, + conference: conference, + media: media, + type: 'contact', + label: label, + id: uuid.v4(), + tags: ['favorite'] + }; + favoriteContacts.push(item); + }); + + return favoriteContacts; + } + getServerHistory() { utils.timestampedLog('Requesting call history from server'); this.props.localHistory.forEach((item) => { if (!item.tags) { item.tags = ['local']; } else if (item.tags.indexOf('local') === -1) { item.tags.push('local'); } }); //let history = this.props.localHistory; let history = []; let getServerCallHistory = new DigestAuthRequest( 'GET', `${this.props.config.serverCallHistoryUrl}?action=get_history&realm=${this.state.accountId.split('@')[1]}`, this.state.accountId.split('@')[0], this.state.password ); // Disable logging getServerCallHistory.loggingOn = false; getServerCallHistory.request((data) => { if (data.success !== undefined && data.success === false) { console.log('Error getting call history from server', 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) => { elem.conference = false; if (!elem.tags) { elem.tags = []; } if (elem.remoteParty.indexOf('@conference.sip2sip.info') > -1) { return null; } let username = elem.remoteParty.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let contact_obj; if (this.props.contacts) { if (isPhoneNumber) { contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', username); } else { contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', elem.remoteParty); } } if (contact_obj) { elem.displayName = contact_obj.displayName; elem.photo = contact_obj.photo; if (isPhoneNumber) { elem.remoteParty = username; } // TODO update icon here } else { elem.photo = null; } if (elem.remoteParty.indexOf('@videoconference.') > -1) { elem.displayName = 'Conference ' + elem.remoteParty.split('@')[0]; elem.remoteParty = elem.remoteParty.split('@')[0] + '@' + this.props.config.defaultConferenceDomain; elem.conference = true; elem.media = ['audio', 'video', 'chat']; } if (elem.remoteParty === this.state.accountId) { elem.displayName = this.props.myDisplayName || 'Myself'; } if (known.indexOf(elem.remoteParty) <= -1) { elem.type = 'history'; elem.id = uuid.v4(); if (elem.tags.indexOf('history') === -1) { elem.tags.push('history'); } 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('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'; } known.push(elem.remoteParty); return elem; } }); this.props.cacheHistory(history); this.setState({history: history}); } }, (errorCode) => { console.log('Error getting call history from server', errorCode); }); } render() { - //console.log('Render history'); + console.log('Render history'); // TODO: render blocked and favorites also when there is no history - let localHistory = this.getLocalHistory(); + console.log('Favorite URIs', this.state.favoriteUris); - let history = localHistory.concat(this.state.serverHistory); - - let items = history.filter(historyItem => historyItem.remoteParty.startsWith(this.props.targetUri)); + let history = []; + let searchExtraItems = []; + let items= [] - let searchExtraItems = this.props.contacts; + if (this.props.filter === 'favorite') { + let favoriteContact = this.getFavoriteContacts(); + items = favoriteContact.filter(historyItem => historyItem.remoteParty.startsWith(this.props.targetUri)); - if (!this.props.targetUri) { - if (!this.findObjectByKey(items, 'remoteParty', this.echoTest.remoteParty)) { - items.push(this.echoTest); + } else { + let localHistory = this.getLocalHistory(); + history = localHistory.concat(this.state.serverHistory); + searchExtraItems = this.props.contacts; + items = history.filter(historyItem => historyItem.remoteParty.startsWith(this.props.targetUri)); + + if (!this.props.targetUri && !this.props.filter) { + if (!this.findObjectByKey(items, 'remoteParty', this.echoTest.remoteParty)) { + items.push(this.echoTest); + } + if (!this.findObjectByKey(items, 'remoteParty', this.videoTest.remoteParty)) { + items.push(this.videoTest); + } } - if (!this.findObjectByKey(items, 'remoteParty', this.videoTest.remoteParty)) { - items.push(this.videoTest); + + let matchedContacts = []; + 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); } - } - let matchedContacts = []; - 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); } - items = items.concat(matchedContacts); - items.forEach((item) => { item.showActions = false; + if (!item.tags) { item.tags = []; } if (this.state.favoriteUris.indexOf(item.remoteParty) > -1 && item.tags.indexOf('favorite') === -1) { item.tags.push('favorite'); } if (this.state.blockedUris.indexOf(item.remoteParty) > -1 && item.tags.indexOf('blocked') === -1) { item.tags.push('blocked'); } let idx = item.tags.indexOf('blocked'); if (this.state.blockedUris.indexOf(item.remoteParty) === -1 && idx > -1) { item.tags.splice(idx, 1); } idx = item.tags.indexOf('favorite'); if (this.state.favoriteUris.indexOf(item.remoteParty) === -1 && idx > -1) { item.tags.splice(idx, 1); } }); if (this.props.filter) { let filteredItems = []; items.forEach((item) => { if (item.tags.indexOf(this.props.filter) > -1) { filteredItems.push(item); } }); items = filteredItems; } if (items.length === 1) { items[0].showActions = true; } 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, cacheHistory : PropTypes.func, serverHistory : PropTypes.array, localHistory : PropTypes.array, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, setFavoriteUri : PropTypes.func, setBlockedUri : PropTypes.func, deleteHistoryEntry : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, filter : PropTypes.string }; export default HistoryTileBox; diff --git a/app/components/ReadyBox.js b/app/components/ReadyBox.js index 5e7ab4c..8c942c4 100644 --- a/app/components/ReadyBox.js +++ b/app/components/ReadyBox.js @@ -1,280 +1,280 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; // import VizSensor = require('react-visibility-sensor').default; import autoBind from 'auto-bind'; import { View, Platform} from 'react-native'; import { IconButton, Title, Button } from 'react-native-paper'; import ConferenceModal from './ConferenceModal'; import HistoryTileBox from './HistoryTileBox'; import FooterBox from './FooterBox'; import URIInput from './URIInput'; import config from '../config'; import utils from '../utils'; import styles from '../assets/styles/blink/_ReadyBox.scss'; class ReadyBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { targetUri: this.props.missedTargetUri, contacts: this.props.contacts, selectedContact: null, showConferenceModal: false, sticky: false, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, historyFilter: null }; } getTargetUri() { const defaultDomain = this.props.account.id.substring(this.props.account.id.indexOf('@') + 1); return utils.normalizeUri(this.state.targetUri, defaultDomain); } async componentDidMount() { //console.log('Ready now'); if (this.state.targetUri) { console.log('We must call', this.state.targetUri); } } componentWillReceiveProps(props) { this.setState({'filterHistory': null}); } filterHistory(filter) { //console.log('set historyFilter', filter); this.setState({'historyFilter': filter}); this.handleTargetChange(''); } handleTargetChange(value, contact) { let new_value = value; if (contact) { if (this.state.targetUri === contact.uri) { new_value = ''; } } if (this.state.targetUri === value) { new_value = ''; } if (new_value === '') { contact = null; } this.setState({targetUri: new_value, selectedContact: contact}); } handleTargetSelect() { if (this.props.connection === null) { this.props._notificationCenter.postSystemNotification("Server unreachable", {timeout: 2}); return; } // the user pressed enter, start a video call by default if (this.state.targetUri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(this.state.targetUri, {audio: true, video: true}); } else { this.props.startCall(this.getTargetUri(), {audio: true, video: true}); } } showConferenceModal(event) { event.preventDefault(); if (this.state.targetUri.length !== 0) { const uri = `${this.state.targetUri.split('@')[0].replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; this.handleConferenceCall(uri.toLowerCase()); } else { this.setState({showConferenceModal: true}); } } handleAudioCall(event) { if (this.props.connection === null) { this.props._notificationCenter.postSystemNotification("Server unreachable", {timeout: 2}); return; } event.preventDefault(); if (this.state.targetUri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(this.state.targetUri, {audio: true, video: false}); } else { this.props.startCall(this.getTargetUri(), {audio: true, video: false}); } } handleVideoCall(event) { if (this.props.connection === null) { this.props._notificationCenter.postSystemNotification("Server unreachable", {timeout: 2}); return; } event.preventDefault(); if (this.state.targetUri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(this.state.targetUri, {audio: true, video: false}); } else { this.props.startCall(this.getTargetUri(), {audio: true, video: true}); } } handleConferenceCall(targetUri, options={audio: true, video: true}) { if (targetUri) { if (!options.video) { console.log('ReadyBox: Handle audio only conference call to',targetUri); } else { console.log('ReadyBox: Handle video conference call to',targetUri); } this.props.startConference(targetUri, options); } this.setState({showConferenceModal: false}); } render() { //utils.timestampedLog('Render ready'); const defaultDomain = `${config.defaultDomain}`; let uriClass = styles.portraitUriInputBox; let uriGroupClass = styles.portraitUriButtonGroup; let titleClass = styles.portraitTitle; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; if (this.props.isTablet) { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTabletTitle : styles.portraitTabletTitle; } else { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTitle : styles.portraitTitle; } if (this.props.isTablet) { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriButtonGroup : styles.portraitTabletUriButtonGroup; } else { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeUriButtonGroup : styles.portraitUriButtonGroup; } if (this.props.isTablet) { uriClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriInputBox : styles.portraitTabletUriInputBox; } else { uriClass = this.props.orientation === 'landscape' ? styles.landscapeUriInputBox : styles.portraitUriInputBox; } const historyClass = this.props.orientation === 'landscape' ? styles.landscapeHistory : styles.portraitHistory; return ( - {(this.state.favoriteUris.length > 0 || this.state.blockedUris.length > 0) ? + {((this.state.favoriteUris.length > 0 || this.state.blockedUris.length > 0 )|| (this.state.favoriteUris.length === 0 && this.state.historyFilter === 'favorite')) ? {(this.state.favoriteUris.length > 0 && this.state.historyFilter !== 'favorite')? : null} {(this.state.blockedUris.length > 0 && this.state.historyFilter !== 'blocked')? : null} : null} {this.props.isTablet && 0? : null} ); } } ReadyBox.propTypes = { account : PropTypes.object.isRequired, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, startCall : PropTypes.func.isRequired, startConference : PropTypes.func.isRequired, missedTargetUri : PropTypes.string, contacts : PropTypes.array, orientation : PropTypes.string, isTablet : PropTypes.bool, refreshHistory : PropTypes.bool, cacheHistory : PropTypes.func, serverHistory : PropTypes.array, localHistory : PropTypes.array, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, deleteHistoryEntry: PropTypes.func, setFavoriteUri : PropTypes.func, setBlockedUri : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array }; export default ReadyBox;