diff --git a/app/components/CallOverlay.js b/app/components/CallOverlay.js index 9b6300b..63227be 100644 --- a/app/components/CallOverlay.js +++ b/app/components/CallOverlay.js @@ -1,199 +1,208 @@ 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'; import { Colors } from 'react-native-paper'; import styles from '../assets/styles/blink/_AudioCallBox.scss'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class CallOverlay extends React.Component { constructor(props) { super(props); autoBind(this); this.state = { call: this.props.call, callState: this.props.call ? this.props.call.state : null, direction: this.props.call ? this.props.call.direction: null, remoteUri: this.props.remoteUri, remoteDisplayName: this.props.remoteDisplayName, reconnectingCall: this.props.reconnectingCall } this.duration = null; this.finalDuration = null; this.timer = null; this._isMounted = true; } componentDidMount() { if (this.state.call) { if (this.state.call.state === 'established') { this.startTimer(); } this.state.call.on('stateChanged', this.callStateChanged); this.setState({callState: this.state.call.state}); } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (!this._isMounted) { return; } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.call !== null && nextProps.call !== this.state.call) { nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call}); } this.setState({remoteDisplayName: nextProps.remoteDisplayName, remoteUri: nextProps.remoteUri}); } componentWillUnmount() { this._isMounted = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); } callStateChanged(oldState, newState, data) { - if (newState === 'established' && this._isMounted) { + if (newState === 'established' && this._isMounted && !this.props.terminated) { this.startTimer(); } if (newState === 'terminated') { if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); this.finalDuration = this.duration; this.duration = null; this.timer = null; } if (!this._isMounted) { return; } this.setState({callState: newState}); } startTimer() { if (this.timer !== null) { // already armed return; } // TODO: consider using window.requestAnimationFrame const startTime = new Date(); this.duration = moment.duration(new Date() - startTime).format('hh:mm:ss', {trim: false}); this.timer = setInterval(() => { this.duration = moment.duration(new Date() - startTime).format('hh:mm:ss', {trim: false}); if (this.props.show) { this.forceUpdate(); } }, 1000); } render() { let header = null; + if (this.props.terminated) { + clearTimeout(this.timer); + this.duration = null; + this.timer = null; + } + let displayName = this.state.remoteUri; if (this.state.remoteDisplayName && this.state.remoteDisplayName !== this.state.remoteUri) { displayName = this.state.remoteDisplayName; } if (this.props.show) { let callDetail; if (this.duration) { callDetail = {this.duration}; callDetail = 'Duration: ' + this.duration; } else { if (this.state.reconnectingCall) { callDetail = 'Reconnecting the call...'; + } else if (this.props.terminated) { + callDetail = 'Call ended'; } else if (this.state.callState === 'terminated') { callDetail = this.finalDuration ? 'Call ended after ' + this.finalDuration : 'Call ended'; } else { if (this.state.callState) { if (this.state.callState === 'incoming') { callDetail = 'Waiting for incoming call...'; } else { callDetail = toTitleCase(this.state.callState); } } else if (this.state.direction) { callDetail = 'Connecting', this.state.direction, 'call...'; } else { callDetail = 'Connecting...'; } } } if (this.state.remoteUri && this.state.remoteUri.search('videoconference') > -1) { displayName = this.state.remoteUri.split('@')[0]; header = ( ); } else { header = ( ); } } return header } } CallOverlay.propTypes = { show: PropTypes.bool.isRequired, remoteUri: PropTypes.string, remoteDisplayName: PropTypes.string, call: PropTypes.object, connection: PropTypes.object, - reconnectingCall: PropTypes.bool + reconnectingCall: PropTypes.bool, + terminated : PropTypes.bool }; export default CallOverlay; diff --git a/app/components/Conference.js b/app/components/Conference.js index 04c0277..96788c1 100644 --- a/app/components/Conference.js +++ b/app/components/Conference.js @@ -1,401 +1,405 @@ import React 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 ConferenceBox from './ConferenceBox'; import LocalMedia from './LocalMedia'; import config from '../config'; import utils from '../utils'; const DEBUG = debug('blinkrtc:Conference'); debug.enable('*'); class Conference extends React.Component { constructor(props) { super(props); autoBind(this); this.defaultWaitInterval = 90; // until we can connect or reconnect this.waitCounter = 0; this.waitInterval = this.defaultWaitInterval; this.userHangup = false; this.ended = false; this.started = false; this.participants = []; this.state = { currentCall: null, + callState: null, targetUri: this.props.targetUri, callUUID: this.props.callUUID, localMedia: this.props.localMedia, connection: this.props.connection, account: this.props.account, registrationState: this.props.registrationState, startedByPush: this.props.startedByPush, reconnectingCall: this.props.reconnectingCall, myInvitedParties: this.props.myInvitedParties, isFavorite: this.props.favoriteUris.indexOf(this.props.targetUri) > -1 } if (this.props.connection) { this.props.connection.on('stateChanged', this.connectionStateChanged); } if (this.props.participantsToInvite) { this.props.participantsToInvite.forEach((p) => { if (this.participants.indexOf(p) === -1) { this.participants.push(p); } }); } } componentWillUnmount() { this.ended = true; if (this.state.currentCall) { this.state.currentCall.removeListener('stateChanged', this.callStateChanged); } if (this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); } } callStateChanged(oldState, newState, data) { utils.timestampedLog('Conference: callStateChanged', oldState, '->', newState); if (newState === 'established') { this.setState({reconnectingCall: false}); } + this.setState({callState: newState}); } connectionStateChanged(oldState, newState) { switch (newState) { case 'disconnected': if (oldState === 'ready') { utils.timestampedLog('Conference: connection failed, reconnecting the call...'); this.waitInterval = this.defaultWaitInterval; } break; default: break; } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { //console.log('Conference got props'); if (nextProps.account !== null && nextProps.account !== this.props.account) { this.setState({account: nextProps.account}); } this.setState({registrationState: nextProps.registrationState}); if (nextProps.connection !== null && nextProps.connection !== this.state.connection) { this.setState({connection: nextProps.connection}); nextProps.connection.on('stateChanged', this.connectionStateChanged); } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia) { this.setState({localMedia: nextProps.localMedia}); } if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) { this.setState({callUUID: nextProps.callUUID, reconnectingCall: true, currentCall: null}); this.startCallWhenReady(); } this.setState({myInvitedParties: nextProps.myInvitedParties, isFavorite: nextProps.favoriteUris.indexOf(this.props.targetUri) > -1 }); } mediaPlaying() { this.startCallWhenReady(); } canConnect() { if (!this.state.localMedia) { console.log('Conference: no local media'); return false; } if (!this.state.connection) { console.log('Conference: no connection yet'); return false; } if (this.state.connection.state !== 'ready') { console.log('Conference: connection is not ready'); return false; } if (!this.state.account) { console.log('Conference: no account yet'); return false; } if (this.state.registrationState !== 'registered') { console.log('Conference: account not ready yet'); return false; } if (this.state.currentCall) { console.log('Conference: call already in progress'); return false; } return true; } async startCallWhenReady() { utils.timestampedLog('Conference: start conference', this.state.callUUID, 'when ready to', this.props.targetUri); this.waitCounter = 0; //utils.timestampedLog('Conference: waiting for connecting to the conference', this.waitInterval, 'seconds'); let diff = 0; while (this.waitCounter < this.waitInterval) { if (this.userHangup) { this.props.hangupCall(this.state.callUUID, 'user_cancelled_conference'); return; } if (this.state.currentCall) { return; } if (this.waitCounter >= this.waitInterval - 1) { utils.timestampedLog('Conference: cancelling conference', this.state.callUUID); this.props.hangupCall(this.state.callUUID, 'timeout'); } if (!this.canConnect()) { //console.log('Waiting', this.waitCounter); await this._sleep(1000); } else { this.waitCounter = 0; this.start(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } start() { const options = { id: this.state.callUUID, pcConfig: {iceServers: config.iceServers}, localStream: this.state.localMedia, audio: this.props.proposedMedia.audio, video: this.props.proposedMedia.video, offerOptions: { offerToReceiveAudio: false, offerToReceiveVideo: false }, initialParticipants: this.props.participantsToInvite }; utils.timestampedLog('Conference: Sylkrtc.js will start conference call', this.state.callUUID, 'to', this.props.targetUri.toLowerCase()); confCall = this.state.account.joinConference(this.props.targetUri.toLowerCase(), options); if (confCall) { confCall.on('stateChanged', this.callStateChanged); this.setState({currentCall: confCall}); } } saveParticipant(callUUID, room, uri) { console.log('Save saveParticipant', uri); if (this.participants.indexOf(uri) === -1) { this.participants.push(uri); } this.props.saveParticipant(callUUID, room, uri); } showSaveDialog() { if (!this.userHangup) { console.log('No save dialog because user did not hangup') return false; } if (this.state.reconnectingCall) { console.log('No save dialog because call is reconnecting') return false; } if (this.participants.length === 0) { console.log('No show dialog because there are no participants') return false; } if (this.state.isFavorite) { let room = this.state.targetUri.split('@')[0]; let must_display = false; if (this.props.myInvitedParties.hasOwnProperty(room)) { let old_participants = this.state.myInvitedParties[room]; this.participants.forEach((p) => { if (old_participants.indexOf(p) === -1) { console.log(p, 'is not in', old_participants); must_display = true; } }); } if (must_display) { console.log('Show save dialog because we have new participants'); return true; } else { console.log('No save dialog because is already favorite with same participants') return false; } } else { console.log('Show save dialog because is not in favorites'); return true; } return true; } saveConference() { if (!this.state.isFavorite) { this.props.setFavoriteUri(this.props.targetUri); } let room = this.state.targetUri.split('@')[0]; if (this.props.myInvitedParties.hasOwnProperty(room)) { let participants = this.state.myInvitedParties[room]; this.participants.forEach((p) => { if (participants.indexOf(p) === -1) { participants.push(p); } }); this.props.saveInvitedParties(this.props.targetUri, participants); } else { this.props.saveInvitedParties(this.props.targetUri, this.participants); } this.props.hangupCall(this.state.callUUID, 'user_hangup_conference_confirmed'); } hangup(reason='user_hangup_conference') { this.userHangup = true; if (!this.showSaveDialog()) { reason = 'user_hangup_conference_confirmed'; } this.props.hangupCall(this.state.callUUID, reason); if (this.waitCounter > 0) { this.waitCounter = this.waitInterval; } } render() { let box = null; if (this.state.localMedia !== null) { - if (this.state.currentCall != null && (this.state.currentCall.state === 'established')) { + if (this.state.currentCall != null && (this.state.callState === 'established')) { box = ( ); } else { box = ( ); } } else { console.log('Error: cannot start conference without local media'); } return box; } } Conference.propTypes = { notificationCenter : PropTypes.func, account : PropTypes.object, connection : PropTypes.object, registrationState : PropTypes.string, hangupCall : PropTypes.func, saveParticipant : PropTypes.func, saveInvitedParties : PropTypes.func, previousParticipants : PropTypes.array, currentCall : PropTypes.object, localMedia : PropTypes.object, targetUri : PropTypes.string, participantsToInvite : PropTypes.array, generatedVideoTrack : PropTypes.bool, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, callUUID : PropTypes.string, proposedMedia : PropTypes.object, isLandscape : PropTypes.bool, isTablet : PropTypes.bool, muted : PropTypes.bool, defaultDomain : PropTypes.string, startedByPush : PropTypes.bool, inFocus : PropTypes.bool, setFavoriteUri : PropTypes.func, saveInvitedParties : PropTypes.func, reconnectingCall : PropTypes.bool, contacts : PropTypes.array, favoriteUris : PropTypes.array }; export default Conference; diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js index 65c702c..5fbc5ad 100644 --- a/app/components/ConferenceBox.js +++ b/app/components/ConferenceBox.js @@ -1,1160 +1,1163 @@ 'use strict'; import React, {Component, Fragment} from 'react'; import { View, Platform, TouchableWithoutFeedback, Dimensions, SafeAreaView, ScrollView, FlatList } from 'react-native'; import PropTypes from 'prop-types'; import * as sylkrtc from 'react-native-sylkrtc'; import classNames from 'classnames'; import debug from 'react-native-debug'; import superagent from 'superagent'; import autoBind from 'auto-bind'; import { RTCView } from 'react-native-webrtc'; import { IconButton, Appbar, Portal, Modal, Surface, Paragraph } from 'react-native-paper'; import config from '../config'; import utils from '../utils'; //import AudioPlayer from './AudioPlayer'; import ConferenceDrawer from './ConferenceDrawer'; import ConferenceDrawerLog from './ConferenceDrawerLog'; // import ConferenceDrawerFiles from './ConferenceDrawerFiles'; import ConferenceDrawerParticipant from './ConferenceDrawerParticipant'; import ConferenceDrawerParticipantList from './ConferenceDrawerParticipantList'; import ConferenceDrawerSpeakerSelection from './ConferenceDrawerSpeakerSelection'; import ConferenceDrawerSpeakerSelectionWrapper from './ConferenceDrawerSpeakerSelectionWrapper'; import ConferenceHeader from './ConferenceHeader'; import ConferenceCarousel from './ConferenceCarousel'; import ConferenceParticipant from './ConferenceParticipant'; import ConferenceMatrixParticipant from './ConferenceMatrixParticipant'; import ConferenceParticipantSelf from './ConferenceParticipantSelf'; import InviteParticipantsModal from './InviteParticipantsModal'; import ConferenceAudioParticipantList from './ConferenceAudioParticipantList'; import ConferenceAudioParticipant from './ConferenceAudioParticipant'; import styles from '../assets/styles/blink/_ConferenceBox.scss'; const DEBUG = debug('blinkrtc:ConferenceBox'); debug.enable('*'); function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class ConferenceBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { callOverlayVisible: true, ended: false, audioMuted: this.props.muted, videoMuted: !this.props.inFocus, videoMutedbyUser: false, participants: props.call.participants.slice(), showInviteModal: false, showDrawer: false, showFiles: false, shareOverlayVisible: false, showSpeakerSelection: false, activeSpeakers: props.call.activeParticipants.slice(), selfDisplayedLarge: false, eventLog: [], sharedFiles: props.call.sharedFiles.slice(), largeVideoStream: null, previousParticipants: this.props.previousParticipants, inFocus: this.props.inFocus, reconnectingCall: this.props.reconnectingCall, terminated: this.props.terminated }; const friendlyName = this.props.remoteUri.split('@')[0]; //if (window.location.origin.startsWith('file://')) { this.callUrl = `${config.publicUrl}/conference/${friendlyName}`; //} else { // this.callUrl = `${window.location.origin}/conference/${friendlyName}`; //} const emailMessage = `You can join me in the conference using a Web browser at ${this.callUrl} ` + 'or by using the freely available Sylk WebRTC client app at http://sylkserver.com'; const subject = 'Join me, maybe?'; this.emailLink = `mailto:?subject=${encodeURI(subject)}&body=${encodeURI(emailMessage)}`; this.overlayTimer = null; this.logEvent = {}; this.haveVideo = false; this.uploads = []; this.selectSpeaker = 1; this.foundContacts = new Map(); if (this.props.call) { this.lookupContact(this.props.call.localIdentity._uri, this.props.call.localIdentity._displayName); } [ 'error', 'warning', 'info', 'debug' ].forEach((level) => { this.logEvent[level] = ( (action, messages, originator) => { const log = this.state.eventLog.slice(); log.unshift({originator, originator, level: level, action: action, messages: messages}); this.setState({eventLog: log}); } ); }); this.invitedParticipants = new Map(); props.initialParticipants.forEach((uri) => { this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'}) this.lookupContact(uri); }); this.participantsTimer = setInterval(() => { this.updateParticipantsStatus(); }, 5000); } updateParticipantsStatus() { let participants_uris = []; this.state.participants.forEach((p) => { participants_uris.push(p.identity._uri); }); const invitedParties = Array.from(this.invitedParticipants.keys()); //console.log('Invited participants', invitedParties); //console.log('Current participants', participants_uris); let p; let interval; invitedParties.forEach((_uri) => { if (participants_uris.indexOf(_uri) > 0) { this.invitedParticipants.delete(_uri); } p = this.invitedParticipants.get(_uri); interval = Math.floor((Date.now() - p.timestamp) / 1000); //console.log(_uri, 'was invited', interval, 'seconds ago'); if (interval >= 60) { this.invitedParticipants.delete(_uri); this.forceUpdate(); } if (p.status.indexOf('Invited') > -1 && interval > 5) { p.status = '..'; } if (p.status.indexOf('.') > -1) { if (interval > 45) { p.status = 'No answer'; } else { p.status = p.status + '..'; } this.forceUpdate(); } }); } componentDidMount() { for (let p of this.state.participants) { p.on('stateChanged', this.onParticipantStateChanged); p.attach(); } this.props.call.on('participantJoined', this.onParticipantJoined); this.props.call.on('participantLeft', this.onParticipantLeft); this.props.call.on('roomConfigured', this.onConfigureRoom); this.props.call.on('fileSharing', this.onFileSharing); if (this.state.participants.length > 1) { this.armOverlayTimer(); } // attach to ourselves first if there are no other participants if (this.state.participants.length === 0) { setTimeout(() => { const item = { stream: this.props.call.getLocalStreams()[0], identity: this.props.call.localIdentity }; this.selectVideo(item); }); } else { this.state.participants.forEach((p) => { if (p.identity._uri.search('guest.') === -1 && p.identity._uri !== this.props.call.localIdentity._uri) { // used for history item this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], p.identity._uri); this.lookupContact(p.identity._uri, p.identity._displayName); } }); // this.changeResolution(); } if (this.props.call.getLocalStreams()[0].getVideoTracks().length !== 0) { this.haveVideo = true; } if (this.state.videoMuted) { console.log('Started video muted'); this._muteVideo(); } else { console.log('Started video active'); } } componentWillUnmount() { clearTimeout(this.overlayTimer); clearTimeout(this.participantsTimer); this.uploads.forEach((upload) => { this.props.notificationCenter().removeNotification(upload[1]); upload[0].abort(); }) } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.inFocus !== this.state.inFocus) { if (nextProps.inFocus) { if (!this.state.videoMutedbyUser) { this._resumeVideo(); } } else { this._muteVideo(); } this.setState({inFocus: nextProps.inFocus}); } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } this.setState({terminated: nextProps.terminated}); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } lookupContact(uri, displayName) { let photo; let username = uri.split('@')[0]; if (this.props.contacts) { let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', username); } else { var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', uri); } if (contact_obj) { displayName = contact_obj.displayName; photo = contact_obj.photo; if (isPhoneNumber) { uri = username; } } else { if (isPhoneNumber) { uri = username; displayName = toTitleCase(username); } } } const c = {photo: photo, displayName: displayName || toTitleCase(username)}; this.foundContacts.set(uri, c) } onParticipantJoined(p) { DEBUG(`Participant joined: ${p.identity}`); if (p.identity._uri.search('guest.') === -1 && p.identity._uri !== this.props.call.localIdentity._uri) { // used for history item this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], p.identity._uri); } this.lookupContact(p.identity._uri, p.identity._displayName); if (this.invitedParticipants.has(p.identity._uri)) { this.invitedParticipants.delete(p.identity._uri); } // this.refs.audioPlayerParticipantJoined.play(); p.on('stateChanged', this.onParticipantStateChanged); p.attach(); this.setState({ participants: this.state.participants.concat([p]) }); // this.changeResolution(); if (this.state.participants.length > 1) { this.armOverlayTimer(); } else { this.setState({callOverlayVisible: true}); } } onParticipantLeft(p) { DEBUG(`Participant left: ${p.identity}`); // this.refs.audioPlayerParticipantLeft.play(); const participants = this.state.participants.slice(); const idx = participants.indexOf(p); if (idx !== -1) { participants.splice(idx, 1); this.setState({ participants: participants }); } p.detach(true); // this.changeResolution(); if (this.state.participants.length > 1) { this.armOverlayTimer(); } else { this.setState({callOverlayVisible: true}); } } onParticipantStateChanged(oldState, newState) { if (newState === 'established' || newState === null) { this.maybeSwitchLargeVideo(); } } onConfigureRoom(config) { const newState = {}; newState.activeSpeakers = config.activeParticipants; this.setState(newState); if (config.activeParticipants.length === 0) { this.logEvent.info('set speakers to', ['Nobody'], config.originator); } else { const speakers = config.activeParticipants.map((p) => {return p.identity.displayName || p.identity.uri}); this.logEvent.info('set speakers to', speakers, config.originator); } this.maybeSwitchLargeVideo(); } onFileSharing(files) { let stateFiles = this.state.sharedFiles; stateFiles = stateFiles.concat(files); this.setState({sharedFiles: stateFiles}); files.forEach((file)=>{ if (file.session !== this.props.call.id) { this.props.notificationCenter().postFileShared(file, this.showFiles); } }) } onVideoSelected(item) { const participants = this.state.participants.slice(); const idx = participants.indexOf(item); participants.splice(idx, 1); participants.unshift(item); if (item.videoPaused) { item.resumeVideo(); } this.setState({ participants: participants }); } changeResolution() { let stream = this.props.call.getLocalStreams()[0]; if (this.state.participants.length < 2) { this.props.call.scaleLocalTrack(stream, 1.5); } else if (this.state.participants.length < 5) { this.props.call.scaleLocalTrack(stream, 2); } else { this.props.call.scaleLocalTrack(stream, 1); } } selectVideo(item) { DEBUG('Switching video to: %o', item); if (item.stream) { this.setState({selfDisplayedLarge: true, largeVideoStream: item.stream}); } } maybeSwitchLargeVideo() { // Switch the large video to another source, maybe. if (this.state.participants.length === 0 && !this.state.selfDisplayedLarge) { // none of the participants are eligible, show ourselves const item = { stream: this.props.call.getLocalStreams()[0], identity: this.props.call.localIdentity }; this.selectVideo(item); } else if (this.state.selfDisplayedLarge) { this.setState({selfDisplayedLarge: false}); } } handleClipboardButton() { utils.copyToClipboard(this.callUrl); this.props.notificationCenter().postSystemNotification('Join me, maybe?', {body: 'Link copied to the clipboard'}); this.setState({shareOverlayVisible: false}); } handleEmailButton(event) { // if (navigator.userAgent.indexOf('Chrome') > 0) { // let emailWindow = window.open(this.emailLink, '_blank'); // setTimeout(() => { // emailWindow.close(); // }, 500); // } else { // window.open(this.emailLink, '_self'); // } this.setState({shareOverlayVisible: false}); } handleShareOverlayEntered() { this.setState({shareOverlayVisible: true}); } handleShareOverlayExited() { this.setState({shareOverlayVisible: false}); } handleActiveSpeakerSelected(participant, secondVideo=false) { // eslint-disable-line space-infix-ops let newActiveSpeakers = this.state.activeSpeakers.slice(); if (secondVideo) { if (participant.id !== 'none') { if (newActiveSpeakers.length >= 1) { newActiveSpeakers[1] = participant; } else { newActiveSpeakers[0] = participant; } } else { newActiveSpeakers.splice(1,1); } } else { if (participant.id !== 'none') { newActiveSpeakers[0] = participant; } else { newActiveSpeakers.shift(); } } this.props.call.configureRoom(newActiveSpeakers.map((element) => element.publisherId), (error) => { if (error) { // This causes a state update, hence the drawer lists update this.logEvent.error('set speakers failed', [], this.localIdentity); } }); } handleDrop(files) { DEBUG('Dropped file %o', files); this.uploadFiles(files); }; handleFiles(e) { DEBUG('Selected files %o', e.target.files); this.uploadFiles(e.target.files); event.target.value = ''; } toggleSpeakerSelection() { this.setState({showSpeakerSelection: !this.state.showSpeakerSelection}); } startSpeakerSelection(number) { this.selectSpeaker = number; this.toggleSpeakerSelection(); } uploadFiles(files) { for (var key in files) { // is the item a File? if (files.hasOwnProperty(key) && files[key] instanceof File) { let uploadRequest; let complete = false; const filename = files[key].name let progressNotification = this.props.notificationCenter().postFileUploadProgress( filename, (notification) => { if (!complete) { uploadRequest.abort(); this.uploads.splice(this.uploads.indexOf(uploadRequest), 1); } } ); uploadRequest = superagent .post(`${config.fileSharingUrl}/${this.props.remoteUri}/${this.props.call.id}/${filename}`) .send(files[key]) .on('progress', (e) => { this.props.notificationCenter().editFileUploadNotification(e.percent, progressNotification); }) .end((err, response) => { complete = true; this.props.notificationCenter().removeFileUploadNotification(progressNotification); if (err) { this.props.notificationCenter().postFileUploadFailed(filename); } this.uploads.splice(this.uploads.indexOf(uploadRequest), 1); }); this.uploads.push([uploadRequest, progressNotification]); } } } downloadFile(filename) { // const a = document.createElement('a'); // a.href = `${config.fileSharingUrl}/${this.props.remoteUri}/${this.props.call.id}/${filename}`; // a.target = '_blank'; // a.download = filename; // const clickEvent = document.createEvent('MouseEvent'); // clickEvent.initMouseEvent('click', true, true, window, 0, // clickEvent.screenX, clickEvent.screenY, clickEvent.clientX, clickEvent.clientY, // clickEvent.ctrlKey, clickEvent.altKey, clickEvent.shiftKey, clickEvent.metaKey, // 0, null); // a.dispatchEvent(clickEvent); } preventOverlay(event) { // Stop the overlay when we are the thumbnail bar event.stopPropagation(); } muteAudio(event) { event.preventDefault(); this.props.toggleMute(this.props.call.id, !this.state.audioMuted); } toggleCamera(event) { event.preventDefault(); const localStream = this.props.call.getLocalStreams()[0]; if (localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; track._switchCamera(); } } muteVideo(event) { event.preventDefault(); if (this.state.videoMuted) { this._resumeVideo(); this.setState({videoMutedbyUser: false}); } else { this.setState({videoMutedbyUser: true}); this._muteVideo(); } } _muteVideo() { const localStream = this.props.call.getLocalStreams()[0]; if (localStream && localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; if (!this.state.videoMuted) { DEBUG('Mute camera'); track.enabled = false; this.setState({videoMuted: true}); } } } _resumeVideo() { const localStream = this.props.call.getLocalStreams()[0]; if (localStream && localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; if (this.state.videoMuted) { DEBUG('Resume camera'); track.enabled = true; this.setState({videoMuted: false}); } } } hangup(event) { event.preventDefault(); for (let participant of this.state.participants) { participant.detach(); } this.props.hangup('user_hangup_conference'); } armOverlayTimer() { if (this.props.audioOnly) { return; } clearTimeout(this.overlayTimer); this.overlayTimer = setTimeout(() => { this.setState({callOverlayVisible: false}); }, 4000); } showOverlay() { if (this.props.audioOnly) { return; } // if (!this.state.shareOverlayVisible && !this.state.showDrawer && !this.state.showFiles) { // if (!this.state.callOverlayVisible) { this.setState({callOverlayVisible: !this.state.callOverlayVisible}); // } // this.armOverlayTimer(); // } } toggleInviteModal() { this.setState({showInviteModal: !this.state.showInviteModal}); } toggleDrawer() { this.setState({callOverlayVisible: true, showDrawer: !this.state.showDrawer, showFiles: false, showSpeakerSelection: false}); clearTimeout(this.overlayTimer); } toggleFiles() { this.setState({callOverlayVisible: true, showFiles: !this.state.showFiles, showDrawer: false}); clearTimeout(this.overlayTimer); } showFiles() { this.setState({callOverlayVisible: true, showFiles: true, showDrawer: false}); clearTimeout(this.overlayTimer); } inviteParticipants(uris) { this.props.call.inviteParticipants(uris); uris.forEach((uri) => { uri = uri.replace(/ /g, ''); if (this.props.call.localIdentity._uri === uri) { return; } this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'}) this.props.saveParticipant(this.props.call.id, this.props.remoteUri.split('@')[0], uri); this.lookupContact(uri); }); this.forceUpdate() } render() { if (this.props.call === null) { return (); } let watermark; const largeVideoClasses = classNames({ 'animated' : true, 'fadeIn' : true, 'large' : true, 'mirror' : !this.props.call.sharingScreen && !this.props.generatedVideoTrack, 'fit' : this.props.call.sharingScreen }); let matrixClasses = classNames({ 'matrix' : true }); const containerClasses = classNames({ 'video-container': true, 'conference': true, 'drawer-visible': this.state.showDrawer || this.state.showFiles }); const remoteUri = this.props.remoteUri.split('@')[0]; // const shareOverlay = ( // // // // // Invite other online users of this service, share this link with others or email, so they can easily join this conference. // // // // // // // // // // // // ); const buttons = {}; // const commonButtonTopClasses = classNames({ // 'btn' : true, // 'btn-link' : true // }); // const fullScreenButtonIcons = classNames({ // 'fa' : true, // 'fa-2x' : true, // 'fa-expand' : !this.isFullScreen(), // 'fa-compress' : this.isFullScreen() // }); const topButtons = []; // if (!this.state.showFiles) { // if (this.state.sharedFiles.length !== 0) { // topButtons.push( // // // // ); // } // } if (!this.state.showDrawer) { topButtons.push(); } buttons.top = {right: topButtons}; const muteButtonIcons = this.state.audioMuted ? 'microphone-off' : 'microphone'; const muteVideoButtonIcons = this.state.videoMuted ? 'video-off' : 'video'; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; const bottomButtons = []; if (!this.state.reconnectingCall) { bottomButtons.push( ); } if (this.haveVideo) { bottomButtons.push( ); } bottomButtons.push( ); if (this.haveVideo) { bottomButtons.push( ); } if (!this.state.reconnectingCall) { bottomButtons.push( ) // bottomButtons.push( // // // // ); } bottomButtons.push( ); buttons.bottom = bottomButtons; const audioParticipants = []; let _contact; let _identity; let participants_uris = []; if (this.props.audioOnly) { _contact = this.foundContacts.get(this.props.call.localIdentity._uri); _identity = {uri: this.props.call.localIdentity._uri, displayName: _contact.displayName, photo: _contact.photo }; participants_uris.push(this.props.call.localIdentity._uri); audioParticipants.push( ); this.state.participants.forEach((p) => { _contact = this.foundContacts.get(p.identity._uri); _identity = {uri: p.identity._uri.indexOf('@guest') > -1 ? 'From the web': p.identity._uri, displayName: (_contact && _contact.displayName != p.identity._displayName) ? _contact.displayName : p.identity._displayName, photo: _contact ? _contact.photo: null }; participants_uris.push(p.identity._uri); audioParticipants.push( ); }); const invitedParties = Array.from(this.invitedParticipants.keys()); let p; invitedParties.forEach((_uri) => { if (participants_uris.indexOf(_uri) > 0) { return; } p = this.invitedParticipants.get(_uri); _contact = this.foundContacts.get(_uri); _identity = {uri: _uri, displayName: (_contact && _contact.displayName ) ? _contact.displayName : _uri, photo: _contact ? _contact.photo: null }; audioParticipants.push( ); }); const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : []; return ( {audioParticipants} {return p.identity.uri})} close={this.toggleInviteModal} room={this.props.remoteUri.split('@')[0]} defaultDomain = {this.props.defaultDomain} accountId = {this.props.call.localIdentity._uri} notificationCenter = {this.props.notificationCenter} /> {drawerParticipants} ); } const participants = []; const drawerParticipants = []; if (this.state.participants.length > 0) { if (this.state.activeSpeakers.findIndex((element) => {return element.id === this.props.call.id}) === -1) { participants.push( ); } } drawerParticipants.push( ); let videos = []; if (this.state.participants.length === 0) { videos.push( ); } else { const activeSpeakers = this.state.activeSpeakers; const activeSpeakersCount = activeSpeakers.length; if (activeSpeakersCount > 0) { activeSpeakers.forEach((p) => { videos.push( ); }); this.state.participants.forEach((p) => { if (this.state.activeSpeakers.indexOf(p) === -1) { participants.push( {}} pauseVideo={true} display={false} /> ); } drawerParticipants.push( ); }); } else { this.state.participants.forEach((p, idx) => { videos.push( = 4) || (idx >= 2 && this.props.isTablet === false)} isLandscape={this.props.isLandscape} isTablet={this.props.isTablet} useTwoRows={this.state.participants.length > 2} /> ); if (idx >= 4 || idx >= 2 && this.props.isTablet === false) { participants.push( ); } drawerParticipants.push( ); }); } } // let filesDrawerContent = ( // // ); const currentParticipants = this.state.participants.map((p) => {return p.identity.uri}) const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : []; return ( {videos} {participants} {drawerParticipants} ); } } ConferenceBox.propTypes = { notificationCenter : PropTypes.func.isRequired, call : PropTypes.object, connection : PropTypes.object, hangup : PropTypes.func, saveParticipant : PropTypes.func, previousParticipants: PropTypes.array, remoteUri : PropTypes.string, generatedVideoTrack : PropTypes.bool, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, isLandscape : PropTypes.bool, isTablet : PropTypes.bool, muted : PropTypes.bool, defaultDomain : PropTypes.string, inFocus : PropTypes.bool, reconnectingCall : PropTypes.bool, audioOnly : PropTypes.bool, contacts : PropTypes.array, - initialParticipants : PropTypes.array + initialParticipants : PropTypes.array, + terminated : PropTypes.bool }; export default ConferenceBox; diff --git a/app/components/ConferenceHeader.js b/app/components/ConferenceHeader.js index d4cf654..bb8b86c 100644 --- a/app/components/ConferenceHeader.js +++ b/app/components/ConferenceHeader.js @@ -1,95 +1,98 @@ import React, { useState, useEffect, useRef, Fragment } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import { Text, Appbar } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import styles from '../assets/styles/blink/_ConferenceHeader.scss'; const useInterval = (callback, delay) => { const savedCallback = useRef(); // Remember the latest callback. useEffect(() => { savedCallback.current = callback; }, [callback]); // Set up the interval. useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); } const ConferenceHeader = (props) => { let [seconds, setSeconds] = useState(0); useInterval(() => { setSeconds(seconds + 1); }, 1000); const duration = moment.duration(seconds, 'seconds').format('hh:mm:ss', {trim: false}); let videoHeader; let callButtons; if (props.show) { const participantCount = props.participants.length + 1; // const callDetail = ( // // {duration} - {participantCount} participant{participantCount > 1 ? 's' : ''} // // ); const room = props.remoteUri.split('@')[0]; let callDetail; if (props.reconnectingCall) { callDetail = 'Reconnecting call...'; + } else if (props.terminated) { + callDetail = 'Call ended'; } else { callDetail = `Duration: ${duration} - ${participantCount} participant${participantCount > 1 ? 's' : ''}`; } videoHeader = ( {props.audioOnly ? null : props.buttons.top.right} ); callButtons = ( {props.buttons.bottom} ); } return ( {videoHeader} {callButtons} ); } ConferenceHeader.propTypes = { show: PropTypes.bool.isRequired, remoteUri: PropTypes.string.isRequired, participants: PropTypes.array.isRequired, buttons: PropTypes.object.isRequired, reconnectingCall: PropTypes.bool, - audioOnly: PropTypes.bool + audioOnly: PropTypes.bool, + terminated: PropTypes.bool }; export default ConferenceHeader; diff --git a/app/components/ConferenceModal.js b/app/components/ConferenceModal.js index 4cc2529..e788452 100644 --- a/app/components/ConferenceModal.js +++ b/app/components/ConferenceModal.js @@ -1,194 +1,194 @@ 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 = { targetUri: props.targetUri ? props.targetUri.split('@')[0] : '', myInvitedParties: props.myInvitedParties, selectedContact: props.selectedContact, participants: null }; this.handleConferenceTargetChange = this.handleConferenceTargetChange.bind(this); this.onHide = this.onHide.bind(this); this.joinAudio = this.joinAudio.bind(this); this.joinVideo = this.joinVideo.bind(this); } componentDidMount() { this.handleConferenceTargetChange(this.state.targetUri); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { let uri = ''; if (nextProps.targetUri) { uri = nextProps.targetUri.split('@')[0]; } this.setState({targetUri: uri, myInvitedParties: nextProps.myInvitedParties, selectedContact: nextProps.selectedContact, participants: nextProps.participants }); this.handleConferenceTargetChange(uri); } handleConferenceTargetChange(value) { let targetUri = value; - let participants = ''; + let participants = []; let sanitizedParticipants = []; let username; let domain; if (targetUri) { let uri = `${targetUri.replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; uri = uri.split('@')[0]; if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(uri)) { participants = this.props.myInvitedParties[uri]; } else if (this.state.selectedContact && this.state.selectedContact.participants) { participants = this.state.selectedContact.participants; } participants.forEach((item) => { item = item.trim().toLowerCase(); if (item === this.props.accountId) { return; } if (item.indexOf('@') === -1) { sanitizedParticipants.push(item); } else { const domain = item.split('@')[1]; if (domain === this.props.defaultDomain) { sanitizedParticipants.push(item.split('@')[0]); } else { sanitizedParticipants.push(item); } } }); } if (targetUri) { this.setState({targetUri: targetUri}); } this.setState({participants: sanitizedParticipants.toString().replace(/,/g, ", ")}); } joinAudio(event) { event.preventDefault(); const uri = `${this.state.targetUri.replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; const participants = []; if (this.state.participants) { this.state.participants.split(',').forEach((item) => { item = item.trim().toLowerCase().replace(' ', ''); if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } participants.push(item); }); } this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: false, participants: participants}); } joinVideo(event) { event.preventDefault(); const uri = `${this.state.targetUri.replace(/[\s()-]/g, '')}@${config.defaultConferenceDomain}`; const participants = []; if (this.state.participants) { this.state.participants.split(',').forEach((item) => { item = item.trim().toLowerCase().replace(' ', ''); if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } participants.push(item); }); } this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: true, participants: participants}); } onHide() { this.props.handleConferenceCall(null); } render() { const validUri = this.state.targetUri.length > 0 && this.state.targetUri.indexOf('@') === -1; return ( Join Conference {this.handleConferenceTargetChange(text);}} name="uri" required defaultValue={this.state.targetUri} /> {this.setState({participants: value});}} value={this.state.participants} placeholder="Enter accounts separated by ," /> ); } } ConferenceModal.propTypes = { show: PropTypes.bool.isRequired, handleConferenceCall: PropTypes.func.isRequired, myInvitedParties: PropTypes.object, accountId: PropTypes.string, selectedContact: PropTypes.object, targetUri: PropTypes.string.isRequired, defaultDomain: PropTypes.string }; export default ConferenceModal; diff --git a/app/components/EditConferenceModal.js b/app/components/EditConferenceModal.js index 9736f90..393f0fd 100644 --- a/app/components/EditConferenceModal.js +++ b/app/components/EditConferenceModal.js @@ -1,120 +1,151 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Chip, Dialog, Portal, Text, Button, Surface, TextInput, Paragraph } 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/_InviteParticipantsModal.scss'; + class EditConferenceModal extends Component { constructor(props) { super(props); autoBind(this); - let users = ''; + let participants = []; if (this.props.invitedParties && this.props.invitedParties.length > 0) { - users = this.props.invitedParties; + participants = this.props.invitedParties; } else if (this.props.selectedContact && this.props.selectedContact.participants) { - users = this.props.selectedContact.participants; + participants = this.props.selectedContact.participants; } this.state = { - users: users.toString(), + participants: this.sanitizedParticipants(participants), selectedContact: this.props.selectedContact, invitedParties: this.props.invitedParties } } + sanitizedParticipants(participants) { + sanitizedParticipants = []; + participants.forEach((item) => { + item = item.trim().toLowerCase(); + + if (item === this.props.accountId) { + return; + } + + if (item.indexOf('@') === -1) { + sanitizedParticipants.push(item); + } else { + const domain = item.split('@')[1]; + if (domain === this.props.defaultDomain) { + sanitizedParticipants.push(item.split('@')[0]); + } else { + sanitizedParticipants.push(item); + } + } + }); + + return sanitizedParticipants.toString().replace(/,/g, ", "); + } + + UNSAFE_componentWillReceiveProps(nextProps) { - let users = ''; + let participants = []; if (nextProps.invitedParties && nextProps.invitedParties.length > 0) { - users = nextProps.invitedParties; + participants = nextProps.invitedParties; } else if (nextProps.selectedContact && nextProps.selectedContact.participants) { - users = nextProps.selectedContact.participants; + participants = nextProps.selectedContact.participants; } this.setState({ - users: users.toString(), + participants: this.sanitizedParticipants(participants), selectedContact: nextProps.selectedContact, invitedParties: nextProps.invitedParties }); } saveParticipants(event) { event.preventDefault(); const uris = []; - if (this.state.users) { - this.state.users.split(',').forEach((item) => { + if (this.state.participants) { + this.state.participants.split(',').forEach((item) => { item = item.trim(); if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } + if (item === this.props.accountId) { + return; + } + let username = item.split('@')[0]; if (username && username !== ',') { uris.push(item); } }); } if (uris) { this.props.saveInvitedParties(uris); - this.setState({users: null}); + this.setState({participants: null}); } this.props.close(); } onInputChange(value) { - this.setState({users: value}); + this.setState({participants: value}); } render() { return ( {this.props.room} People you wish to invite when you join the room: ); } } EditConferenceModal.propTypes = { - show: PropTypes.bool.isRequired, - close: PropTypes.func.isRequired, - saveInvitedParties: PropTypes.func, - invitedParties: PropTypes.array, - room: PropTypes.string, - selectedContact: PropTypes.object, - defaultDomain: PropTypes.string + show : PropTypes.bool.isRequired, + close : PropTypes.func.isRequired, + saveInvitedParties : PropTypes.func, + invitedParties : PropTypes.array, + room : PropTypes.string, + selectedContact : PropTypes.object, + defaultDomain : PropTypes.string, + accountId : PropTypes.string }; export default EditConferenceModal; diff --git a/app/components/HistoryCard.js b/app/components/HistoryCard.js index d920e86..53687b1 100644 --- a/app/components/HistoryCard.js +++ b/app/components/HistoryCard.js @@ -1,455 +1,457 @@ import React, { Component, Fragment} 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 EditConferenceModal from './EditConferenceModal'; import styles from '../assets/styles/blink/_HistoryCard.scss'; import UserIcon from './UserIcon'; import utils from '../utils'; 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? {displayName} ({uri}) : {uri} } ); const renderItem = ({ item }) => ( ); function isIp(ipaddress) { if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { return (true) } return (false) } class HistoryCard extends Component { constructor(props) { super(props); autoBind(this); this.state = { id: this.props.contact.id, contact: this.props.contact, displayName: this.props.contact.displayName, filter: this.props.filter, uri: this.props.contact.remoteParty, invitedParties: this.props.invitedParties, 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, confirmed: false, showEditConferenceModal: false } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ id: nextProps.contact.id, contact: nextProps.contact, displayName: nextProps.contact.displayName, uri: nextProps.contact.remoteParty, invitedParties: nextProps.invitedParties, participants: nextProps.contact.participants, conference: nextProps.contact.conference, type: nextProps.contact.type, photo: nextProps.contact.photo, label: nextProps.contact.label, orientation: nextProps.orientation, favorite: (nextProps.contact.tags.indexOf('favorite') > -1)? true : false, blocked: (nextProps.contact.tags.indexOf('blocked') > -1)? true : false, }); } shouldComponentUpdate(nextProps) { //https://medium.com/sanjagh/how-to-optimize-your-react-native-flatlist-946490c8c49b return true; } toggleEditConferenceModal() { this.setState({showEditConferenceModal: !this.state.showEditConferenceModal}); } setFavoriteUri() { if (this.state.favorite) { if (this.state.confirmed) { let newFavoriteState = this.props.setFavoriteUri(this.state.uri); this.setState({favorite: newFavoriteState, action: null, confirmed: false}); this.props.setTargetUri(this.state.uri); } else { this.setState({confirmed: true}); } } else { let newFavoriteState = this.props.setFavoriteUri(this.state.uri); this.setState({favorite: newFavoriteState}); } } saveInvitedParties(uris) { this.setState({invitedParties: uris}); this.props.saveInvitedParties(this.state.uri.split('@')[0], uris); } 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}); } setBlockedDomain() { let newBlockedState = this.props.setBlockedUri('@' + this.state.uri.split('@')[1]); this.setState({blocked: newBlockedState}); } editConference() { this.toggleEditConferenceModal(); } deleteHistoryEntry() { this.props.deleteHistoryEntry(this.state.uri); } undo() { this.setState({confirmed: false, action: null}); } setFavoriteUri() { if (this.state.favorite) { if (this.state.confirmed) { let newFavoriteState = this.props.setFavoriteUri(this.state.uri); this.setState({favorite: newFavoriteState, action: null, confirmed: false}); this.props.setTargetUri(this.state.uri); } else { this.setState({confirmed: true}); } } else { let newFavoriteState = this.props.setFavoriteUri(this.state.uri); this.setState({favorite: newFavoriteState}); } } setTargetUri(uri, contact) { this.props.setTargetUri(uri, this.state.contact); } render () { let containerClass = styles.portraitContainer; let cardClass = styles.card; let showActions = this.props.contact.showActions && this.props.contact.tags.indexOf('test') === -1; let uri = this.state.uri; let username = uri.split('@')[0]; let domain = uri.split('@')[1]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let displayName = this.state.displayName; let buttonMode = 'text'; let showBlockButton = !this.state.conference; let showBlockDomainButton = false; let showFavoriteButton = true; let showUndoButton = this.state.confirmed ? true : false; let showDeleteButton = (this.props.contact.tags.indexOf('local') > -1 && !this.state.favorite) ? true: false; let showEditButton = (this.state.conference && this.state.favorite && !this.state.confirmed ) ? true: false; let blockTextbutton = 'Block'; let blockDomainTextbutton = 'Block domain'; let editTextbutton = 'Edit'; let favoriteTextbutton = 'Favorite'; let undoTextbutton = 'Undo'; let deleteTextbutton = 'Delete'; let participantsData = []; if (this.state.favorite) { favoriteTextbutton = this.state.confirmed ? 'Confirm' : 'Remove'; if (!this.state.blocked) { showBlockButton = false; } } if (uri.indexOf('3333@') > -1) { showBlockButton = false; } if (uri.indexOf('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 || username; let subtitle = uri; let description = this.props.contact.startTime; if (displayName === uri) { title = toTitleCase(username); } if (isPhoneNumber && isIp(domain)) { title = 'Tel ' + username; subtitle = 'From @' + domain; showBlockDomainButton = true; showFavoriteButton = false; } if (utils.isAnonymous(uri)) { //uri = 'anonymous@anonymous.invalid'; displayName = 'Anonymous'; if (uri.indexOf('@guest.') > -1) { subtitle = 'From the Web'; } showFavoriteButton = false; showBlockDomainButton = true; if (!this.state.blocked) { showBlockButton = false; } blockDomainTextbutton = 'Block Web calls'; } if (!username || username.length === 0) { if (isIp(domain)) { title = 'IP domain'; } else if (domain.indexOf('guest.') > -1) { title = 'Calls from the Web'; } else { title = 'Domain'; } } if (this.props.contact.tags.indexOf('history') > -1) { 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) { let participants = this.state.participants; if (this.state.filter === 'favorite' && this.state.invitedParties && this.state.invitedParties.length > 0 ) { participants = this.state.invitedParties; } if (participants && participants.length > 0) { const p_text = participants.length > 1 ? 'participants' : 'participant'; subtitle = 'With ' + participants.length + ' ' + p_text; let i = 1; let contact_obj; let dn; let _item; participants.forEach((participant) => { contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', participant); dn = contact_obj ? contact_obj.displayName : participant; _item = {nr: i, id: uuid.v4(), uri: participant, displayName: dn}; participantsData.push(_item); i = i + 1; }); } else { subtitle = 'With no participants'; } let dn; if (participantsData.length > 4 || participantsData.length < 2) { title = username.length > 10 ? 'Conference' : toTitleCase(username); } else if (participantsData.length > 1 || participantsData.length <= 4 ) { let j = 0; if (username.length < 10) { title = toTitleCase(username); } else { participantsData.forEach((participant) => { if (participant.displayName === participant.uri) { dn = toTitleCase(participant.uri.split('@')[0]); } else { dn = participant.displayName.split(' ')[0]; } title = title + dn; if (j < participantsData.length - 1) { title = title + ' & '; } j = j + 1; }); } } } 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} {participantsData && participantsData.length && showActions ? item.id} key={item => item.id} /> : null} {showActions ? {showEditButton? : null} {showDeleteButton? : null} {showBlockButton? : null} {showBlockDomainButton? : null} {showFavoriteButton?: null} {showUndoButton?: null} : null} ); } else { return ( {this.props.setTargetUri(uri, this.props.contact)}} style={[containerClass, cardClass]} > {title} {uri} {this.state.label} {showActions ? {showBlockButton? : null} {showFavoriteButton?: null} {showUndoButton?: null} : null} ); } } } HistoryCard.propTypes = { id : PropTypes.string, contact : PropTypes.object, setTargetUri : PropTypes.func, setBlockedUri : PropTypes.func, setFavoriteUri : PropTypes.func, saveInvitedParties : PropTypes.func, deleteHistoryEntry : PropTypes.func, orientation : PropTypes.string, isTablet : PropTypes.bool, contacts : PropTypes.array, - defaultDomain : PropTypes.string + defaultDomain : PropTypes.string, + accountId : PropTypes.string }; export default HistoryCard; diff --git a/app/components/HistoryTileBox.js b/app/components/HistoryTileBox.js index c111c64..fc98e25 100644 --- a/app/components/HistoryTileBox.js +++ b/app/components/HistoryTileBox.js @@ -1,623 +1,624 @@ 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 moment from 'moment'; import momenttz from 'moment-timezone'; import styles from '../assets/styles/blink/_HistoryTileBox.scss'; class HistoryTileBox extends Component { constructor(props) { super(props); autoBind(this); this.favoriteContacts = []; this.state = { serverHistory: this.props.serverHistory, localHistory: this.props.localHistory, accountId: this.props.account ? this.props.account.id : '', password: this.props.password, targetUri: this.props.targetUri, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, isRefreshing: false, contacts: this.props.contacts, myInvitedParties: this.props.myInvitedParties, refreshHistory: this.props.refreshHistory } 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); this.ended = false; } componentDidMount() { this.ended = false; this.getFavoriteContacts(); } componentWillUnmount() { this.ended = true; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (this.ended) { return; } if (nextProps.myInvitedParties !== this.state.myInvitedParties) { this.setState({myInvitedParties: nextProps.myInvitedParties}); } if (nextProps.contacts !== this.state.contacts) { this.setState({contacts: nextProps.contacts}); } if (nextProps.favoriteUris !== this.state.favoriteUris) { this.setState({favoriteUris: nextProps.favoriteUris}); } if (nextProps.account !== null && nextProps.account !== this.props.account) { this.setState({accountId: nextProps.account.id}); } if (nextProps.refreshHistory !== this.state.refreshHistory) { this.setState({refreshHistory: nextProps.refreshHistory}); this.getServerHistory(); } this.getFavoriteContacts(); } 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); } saveInvitedParties(room, uris) { if (this.ended) { return; } this.props.saveInvitedParties(room, uris); let myInvitedParties = this.state.myInvitedParties; if (myInvitedParties && myInvitedParties.hasOwnProperty(room)) { myInvitedParties[room] = uris; this.setState({myInvitedParties: myInvitedParties}); } } setBlockedUri(uri) { return this.props.setBlockedUri(uri); } renderItem(item) { let invitedParties = []; let uri = item.item.remoteParty; let username = uri.split('@')[0]; if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(username)) { invitedParties = this.state.myInvitedParties[username]; } return( ); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } getLocalHistory() { let history = this.state.localHistory; history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) let known = []; let uri; history = history.filter((elem) => { uri = elem.remoteParty.toLowerCase(); if (uri.indexOf('@videoconference') === -1) { return; } if (known.indexOf(uri) <= -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(uri); return elem; } }); return history; } getFavoriteContacts() { console.log('Update favorite contacts'); let favoriteContacts = []; let displayName; let label; let conference; let metadata = ''; let contacts = this.state.contacts; contacts = contacts.concat(this.videoTest); contacts = contacts.concat(this.echoTest); this.state.favoriteUris.forEach((uri) => { if (!uri) { return; } uri = uri.toLowerCase(); const contact_obj = this.findObjectByKey(contacts, 'remoteParty', uri); displayName = contact_obj ? contact_obj.displayName : uri; label = contact_obj ? contact_obj.label: null; conference = false; let tags = ['favorite']; const history_obj = this.findObjectByKey(this.state.serverHistory, 'remoteParty', uri); const startTime = history_obj? history_obj.startTime : null; const stopTime = history_obj? history_obj.stopTime : null; const duration = history_obj? history_obj.duration : 0; let media = history_obj? history_obj.media : ['audio']; tags.push('history'); if (uri.indexOf('@videoconference.') > -1) { displayName = uri.split('@')[0]; const room = uri.split('@')[0]; uri = room + '@' + this.props.config.defaultConferenceDomain; conference = true; media = ['audio', 'video', 'chat']; tags.push('conference'); if (this.state.myInvitedParties.hasOwnProperty(room)) { metadata = this.state.myInvitedParties[room].toString(); } } const item = { remoteParty: uri, metadata: metadata, displayName: displayName, conference: conference, media: media, type: 'contact', startTime: startTime, startTime: startTime, duration: duration, label: label, id: uuid.v4(), tags: tags }; favoriteContacts.push(item); }); this.favoriteContacts = favoriteContacts; } getBlockedContacts() { let blockedContacts = []; let contact_obj; let displayName; let label; let contacts= this.state.contacts contacts = contacts.concat(this.videoTest); contacts = contacts.concat(this.echoTest); this.state.blockedUris.forEach((uri) => { contact_obj = this.findObjectByKey(contacts, 'remoteParty', uri); displayName = contact_obj ? contact_obj.displayName : uri; label = contact_obj ? contact_obj.label: null; const item = { remoteParty: uri.toLowerCase(), displayName: displayName, conference: false, type: 'contact', label: label, id: uuid.v4(), tags: ['blocked'] }; blockedContacts.push(item); }); return blockedContacts; } getServerHistory() { if (this.ended || !this.state.accountId || this.state.isRefreshing) { return; } this.setState({isRefreshing: true}); utils.timestampedLog('Requesting call history from server'); let history = []; let localTime; let hasMissedCalls = false; 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.received) { data.received.map(elem => {elem.direction = 'received'; return elem}); history = history.concat(data.received); } if (data.placed) { data.placed.map(elem => {elem.direction = 'placed'; return elem}); history = history.concat(data.placed); } history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) if (history) { const known = []; history = history.filter((elem) => { elem.conference = false; if (!elem.tags) { elem.tags = []; } if (elem.remoteParty.indexOf('@conference.') > -1) { return null; } elem.remoteParty = elem.remoteParty.toLowerCase(); let username = elem.remoteParty.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let contact_obj; if (this.state.contacts) { if (isPhoneNumber) { contact_obj = this.findObjectByKey(this.state.contacts, 'remoteParty', username); } else { contact_obj = this.findObjectByKey(this.state.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('@guest.') > -1) { elem.remoteParty = elem.displayName.toLowerCase().replace(/ /g, '') + '@' + elem.remoteParty.split('@')[1]; } if (elem.remoteParty.indexOf('@videoconference.') > -1) { elem.displayName = 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'; } 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'; } if (elem.timezone !== undefined) { localTime = momenttz.tz(elem.startTime, elem.timezone).toDate(); elem.startTime = moment(localTime).format('YYYY-MM-DD HH:mm:ss'); localTime = momenttz.tz(elem.stopTime, elem.timezone).toDate(); elem.stopTime = moment(localTime).format('YYYY-MM-DD HH:mm:ss'); } if (known.indexOf(elem.remoteParty) <= -1) { known.push(elem.remoteParty); if (elem.direction === 'received' && elem.duration === 0) { elem.tags.push('missed'); hasMissedCalls = true; } return elem; } }); this.props.cacheHistory(history); if (this.ended) { return; } this.setState({serverHistory: history, isRefreshing: false}); this.props.setMissedCalls(hasMissedCalls); } }, (errorCode) => { console.log('Error getting call history from server', errorCode); }); this.setState({isRefreshing: false}); } matchContact(contact, filter='') { if (contact.remoteParty.toLowerCase().startsWith(filter.toLowerCase())) { return true; } if (contact.displayName && contact.displayName.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return true; } if (contact.metadata && filter.length > 2 && contact.metadata.indexOf(filter) > -1) { return true; } return false; } render() { let history = []; let searchExtraItems = []; let items = []; let matchedContacts = []; if (this.props.filter === 'favorite') { items = this.favoriteContacts.filter(historyItem => this.matchContact(historyItem, this.props.targetUri)); } else if (this.props.filter === 'blocked') { let blockedContacts = this.getBlockedContacts(); items = blockedContacts.filter(historyItem => this.matchContact(historyItem, this.props.targetUri)); } else if (this.props.filter === 'missed') { history = this.state.serverHistory; items = history.filter(historyItem => this.matchContact(historyItem, this.props.targetUri) && historyItem.tags.indexOf('missed') > -1); } else { history = this.getLocalHistory(); history = history.concat(this.state.serverHistory); searchExtraItems = searchExtraItems.concat(this.state.contacts); searchExtraItems = searchExtraItems.concat(this.favoriteContacts); searchExtraItems = searchExtraItems.concat(this.videoTest); searchExtraItems = searchExtraItems.concat(this.echoTest); items = history.filter(historyItem => this.matchContact(historyItem, this.props.targetUri)); if (this.props.targetUri && this.props.targetUri.length > 2 && !this.props.selectedContact) { matchedContacts = searchExtraItems.filter(contact => this.matchContact(contact, this.props.targetUri)); } else if (this.props.selectedContact && this.props.selectedContact.type === 'contact') { matchedContacts.push(this.props.selectedContact); } items = items.concat(matchedContacts); } const known = []; items.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) items = items.filter((elem) => { if (known.indexOf(elem.remoteParty) <= -1) { known.push(elem.remoteParty); if (!elem.startTime) { elem.startTime = '1970-01-01 01:01:01' } return elem; } }); 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); } } 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 (item.remoteParty.indexOf('@videoconference.') === -1) { item.conference = false; } }); let filteredItems = []; items.reverse(); items.forEach((item) => { const fromDomain = '@' + item.remoteParty.split('@')[1]; if (this.props.filter && item.tags.indexOf(this.props.filter) > -1) { filteredItems.push(item); } else if (this.state.blockedUris.indexOf(item.remoteParty) === -1 && this.state.blockedUris.indexOf(fromDomain) === -1) { filteredItems.push(item); } }); items = filteredItems; items.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) 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, 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, saveInvitedParties: PropTypes.func, myInvitedParties: PropTypes.object, setBlockedUri : PropTypes.func, deleteHistoryEntry : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, setMissedCalls : PropTypes.func, filter : PropTypes.string, defaultDomain : PropTypes.string }; export default HistoryTileBox; diff --git a/app/components/LocalMedia.js b/app/components/LocalMedia.js index 6456e99..cf2ba76 100644 --- a/app/components/LocalMedia.js +++ b/app/components/LocalMedia.js @@ -1,137 +1,139 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View, Dimensions } from 'react-native'; import { RTCView } from 'react-native-webrtc'; import { IconButton, Button, Text} from 'react-native-paper'; import CallOverlay from './CallOverlay'; import styles from '../assets/styles/blink/_LocalMedia.scss'; class LocalMedia extends Component { constructor(props) { super(props); autoBind(this); this.localVideo = React.createRef(); this.state = { localMedia: this.props.localMedia, historyEntry: this.props.historyEntry, participants: this.props.participants }; } componentDidMount() { this.props.mediaPlaying(); } //getDerivedStateFromProps(nextProps, state) UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.localMedia && nextProps.localMedia !== this.state.localMedia) { this.props.mediaPlaying(); } this.setState({historyEntry: nextProps.historyEntry, participants: nextProps.participants}); } saveConference(event) { event.preventDefault(); this.props.saveConference(); } showSaveDialog() { if (!this.props.showSaveDialog) { return false; } return this.props.showSaveDialog(); } hangupCall(event) { event.preventDefault(); this.props.hangupCall('user_hangup_conference_confirmed'); } render() { let {height, width} = Dimensions.get('window'); let videoStyle = { height, width }; const streamUrl = this.props.localMedia ? this.props.localMedia.toURL() : null; const buttonSize = this.props.isTablet ? 40 : 34; const buttonContainerClass = this.props.isTablet ? styles.tabletButtonContainer : styles.buttonContainer; return ( {this.showSaveDialog() ? Save conference maybe? Would you like to save participants {this.state.participants.toString().replace(/,/g, ', ')} for having another conference later? You can find later it in your Favorites. : } ); } } LocalMedia.propTypes = { call : PropTypes.object, remoteUri : PropTypes.string, remoteDisplayName : PropTypes.string, localMedia : PropTypes.object.isRequired, mediaPlaying : PropTypes.func.isRequired, hangupCall : PropTypes.func, showSaveDialog : PropTypes.func, saveConference : PropTypes.func, reconnectingCall : PropTypes.bool, connection : PropTypes.object, - participants : PropTypes.array + participants : PropTypes.array, + terminated : PropTypes.bool }; export default LocalMedia;