Page MenuHomePhabricator

No OneTemporary

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 = <View><Icon name="clock"/><Text>{this.duration}</Text></View>;
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 = (
<Appbar.Header style={{backgroundColor: 'black'}}>
<Appbar.Content
title={`Conference: ${displayName}`} subtitle={callDetail}
/>
</Appbar.Header>
);
} else {
header = (
<Appbar.Header style={styles.appbarContainer}>
<Appbar.Content
title={`Call with ${displayName}`} subtitle={callDetail}
/>
</Appbar.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 = (
<ConferenceBox
notificationCenter = {this.props.notificationCenter}
call = {this.state.currentCall}
- audioOnly = {!this.props.proposedMedia.video}
+ audioOnly = {this.props.proposedMedia ? !this.props.proposedMedia.video: false}
reconnectingCall={this.state.reconnectingCall}
connection = {this.state.connection}
hangup = {this.hangup}
saveParticipant = {this.saveParticipant}
saveInvitedParties = {this.props.saveInvitedParties}
previousParticipants = {this.props.previousParticipants}
remoteUri = {this.props.targetUri}
shareScreen = {this.props.shareScreen}
generatedVideoTrack = {this.props.generatedVideoTrack}
speakerPhoneEnabled = {this.props.speakerPhoneEnabled}
toggleMute = {this.props.toggleMute}
toggleSpeakerPhone = {this.props.toggleSpeakerPhone}
isLandscape = {this.props.isLandscape}
isTablet = {this.props.isTablet}
muted = {this.props.muted}
defaultDomain = {this.props.defaultDomain}
inFocus = {this.props.inFocus}
reconnectingCall={this.state.reconnectingCall}
contacts={this.props.contacts}
initialParticipants={this.props.participantsToInvite}
+ terminated={this.userHangup}
/>
);
} else {
box = (
<LocalMedia
call = {this.state.currentCall}
remoteUri = {this.props.targetUri}
remoteDisplayName = {this.props.targetUri}
localMedia = {this.state.localMedia}
mediaPlaying = {this.mediaPlaying}
hangupCall = {this.hangup}
showSaveDialog={this.showSaveDialog}
saveConference={this.saveConference}
connection={this.state.connection}
participants={this.participants}
+ terminated={this.userHangup}
/>
);
}
} 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 (<View></View>);
}
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 = (
// <Portal>
// <Modal>
// <Surface>
// <Paragraph>
// Invite other online users of this service, share <strong><a href={this.callUrl} target="_blank" rel="noopener noreferrer">this link</a></strong> with others or email, so they can easily join this conference.
// </Paragraph>
// <View className="text-center">
// <View className="btn-group">
// <IconButton
// size={30}
// onPress={this.toggleInviteModal}
// icon="account-plus"
// />
// <IconButton className="btn btn-primary" onPress={this.handleClipboardButton} icon="copy" />
// <IconButton className="btn btn-primary" onPress={this.handleEmailButton} alt="Send email" icon="email" />
// </View>
// </View>
// </Surface>
// </Modal>
// </Portal>
// );
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(
// <Badge badgeContent={this.state.sharedFiles.length} color="primary" classes={{badge: this.props.classes.badge}}>
// <button key="fbButton" type="button" title="Open Drawer" className={commonButtonTopClasses} onPress={this.toggleFiles}> <i className="fa fa-files-o fa-2x"></i> </button>
// </Badge>
// );
// }
// }
if (!this.state.showDrawer) {
topButtons.push(<Appbar.Action key="sbButton" title="Open Drawer" onPress={this.toggleDrawer} icon="menu" />);
}
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(
<IconButton
size={30}
style={buttonClass}
title="Share link to this conference"
icon="account-plus"
onPress={this.toggleInviteModal}
key="shareButton"
/>
);
}
if (this.haveVideo) {
bottomButtons.push(
<IconButton
size={30}
style={buttonClass}
title="Mute/unmute video"
onPress={this.muteVideo}
icon={muteVideoButtonIcons}
key="muteButton"
/>
);
}
bottomButtons.push(
<IconButton
size={30}
style={buttonClass}
title="Mute/unmute audio"
onPress={this.muteAudio}
icon={muteButtonIcons}
key="muteAudioButton"
/>
);
if (this.haveVideo) {
bottomButtons.push(
<IconButton
size={30}
style={buttonClass}
title="Toggle camera"
onPress={this.toggleCamera}
icon='video-switch'
key="toggleButton"
/>
);
}
if (!this.state.reconnectingCall) {
bottomButtons.push(
<IconButton
size={30}
style={buttonClass}
icon={this.props.speakerPhoneEnabled ? 'volume-high' : 'volume-off'}
onPress={this.props.toggleSpeakerPhone}
key="speakerPhoneButton"
/>
)
// bottomButtons.push(
// <View key="shareFiles">
// <IconButton size={30} style={buttonClass} title="Share files" component="span" disableRipple={true} icon="upload"/>
// </View>
// );
}
bottomButtons.push(
<IconButton
size={30}
style={[buttonClass, styles.hangupButton]}
title="Leave conference"
onPress={this.hangup}
icon="phone-hangup"
key="hangupButton"
/>
);
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(
<ConferenceAudioParticipant
key="myself"
participant={null}
identity={_identity}
isLocal={true}
/>
);
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(
<ConferenceAudioParticipant
key={p.id}
participant={p}
identity={_identity}
isLocal={false}
/>
);
});
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(
<ConferenceAudioParticipant
key={_uri}
identity={_identity}
isLocal={false}
status={p.status}
/>
);
});
const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : [];
return (
<View style={styles.container}>
<View style={styles.conferenceContainer}>
<ConferenceHeader
show={true}
remoteUri={remoteUri}
participants={this.state.participants}
reconnectingCall={this.state.reconnectingCall}
buttons={buttons}
audioOnly={this.props.audioOnly}
+ terminated={this.state.terminated}
/>
</View>
<View style={styles.audioContainer}>
<ConferenceAudioParticipantList >
{audioParticipants}
</ConferenceAudioParticipantList>
</View>
<InviteParticipantsModal
show={this.state.showInviteModal && !this.state.reconnectingCall}
inviteParticipants={this.inviteParticipants}
previousParticipants={this.state.previousParticipants}
alreadyInvitedParticipants={alreadyInvitedParticipants}
currentParticipants={this.state.participants.map((p) => {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}
/>
<ConferenceDrawer
show={this.state.showDrawer && !this.state.reconnectingCall}
close={this.toggleDrawer}
isLandscape={this.props.isLandscape}
title="Conference data"
>
<View style={this.props.isLandscape ? [{maxHeight: Dimensions.get('window').height - 60}, styles.landscapeDrawer] : styles.container}>
<View style={{flex: this.props.isLandscape ? 1 : 2}}>
<ConferenceDrawerSpeakerSelectionWrapper
selectSpeaker={this.startSpeakerSelection}
activeSpeakers={this.state.activeSpeakers}
/>
<ConferenceDrawerParticipantList style={styles.container}>
{drawerParticipants}
</ConferenceDrawerParticipantList>
</View>
</View>
</ConferenceDrawer>
<ConferenceDrawer
show={this.state.showSpeakerSelection}
close={this.toggleSpeakerSelection}
isLandscape={this.props.isLandscape}
showBackdrop={false}
title={`Select speaker ${this.selectSpeaker}`}
>
<ConferenceDrawerSpeakerSelection
participants={this.state.participants.concat([{id: this.props.call.id, publisherId: this.props.call.id, identity: this.props.call.localIdentity}])}
selected={this.handleActiveSpeakerSelected}
activeSpeakers={this.state.activeSpeakers}
selectSpeaker={this.selectSpeaker}
key = {this.state.activeSpeakers}
/>
</ConferenceDrawer>
</View>
);
}
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(
<ConferenceParticipantSelf
key="myself2"
stream={this.props.call.getLocalStreams()[0]}
identity={this.props.call.localIdentity}
audioMuted={this.state.audioMuted}
generatedVideoTrack={this.props.generatedVideoTrack}
/>
);
}
}
drawerParticipants.push(
<ConferenceDrawerParticipant
key="myself1"
participant={{identity: this.props.call.localIdentity}}
isLocal={true}
/>
);
let videos = [];
if (this.state.participants.length === 0) {
videos.push(
<RTCView key="self" objectFit="cover" style={styles.wholePageVideo} ref="largeVideo" poster="assets/images/transparent-1px.png" streamURL={this.state.largeVideoStream ? this.state.largeVideoStream.toURL() : null} />
);
} else {
const activeSpeakers = this.state.activeSpeakers;
const activeSpeakersCount = activeSpeakers.length;
if (activeSpeakersCount > 0) {
activeSpeakers.forEach((p) => {
videos.push(
<ConferenceMatrixParticipant
key={p.id}
participant={p}
pauseVideo={this.props.audioOnly}
large={activeSpeakers.length <= 1}
isLocal={p.id === this.props.call.id}
/>
);
});
this.state.participants.forEach((p) => {
if (this.state.activeSpeakers.indexOf(p) === -1) {
participants.push(
<ConferenceParticipant
key={p.id}
participant={p}
selected={() => {}}
pauseVideo={true}
display={false}
/>
);
}
drawerParticipants.push(
<ConferenceDrawerParticipant
key={p.id}
participant={p}
/>
);
});
} else {
this.state.participants.forEach((p, idx) => {
videos.push(
<ConferenceMatrixParticipant
key = {p.id}
participant = {p}
large = {this.state.participants.length <= 1}
pauseVideo={(idx >= 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(
<ConferenceParticipant
key={p.id}
participant={p}
selected={this.onVideoSelected}
pauseVideo={true}
display={true}
/>
);
}
drawerParticipants.push(
<ConferenceDrawerParticipant
key={p.id}
participant={p}
/>
);
});
}
}
// let filesDrawerContent = (
// <ConferenceDrawerFiles
// sharedFiles={this.state.sharedFiles}
// downloadFile={this.downloadFile}
// />
// );
const currentParticipants = this.state.participants.map((p) => {return p.identity.uri})
const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : [];
return (
<View style={styles.container}>
<View style={styles.conferenceContainer}>
<ConferenceHeader
show={this.state.callOverlayVisible}
remoteUri={remoteUri}
participants={this.state.participants}
reconnectingCall={this.state.reconnectingCall}
buttons={buttons}
audioOnly={this.props.audioOnly}
+ terminated={this.state.terminated}
/>
<TouchableWithoutFeedback onPress={this.showOverlay}>
<View style={[styles.videosContainer, this.props.isLandscape ? styles.landscapeVideosContainer: null]}>
{videos}
</View>
</TouchableWithoutFeedback>
<View style={styles.carouselContainer}>
<ConferenceCarousel align={'right'}>
{participants}
</ConferenceCarousel>
</View>
</View>
<InviteParticipantsModal
show={this.state.showInviteModal && !this.state.reconnectingCall}
inviteParticipants={this.inviteParticipants}
previousParticipants={this.state.previousParticipants}
currentParticipants={currentParticipants}
alreadyInvitedParticipants={alreadyInvitedParticipants}
close={this.toggleInviteModal}
room={this.props.remoteUri.split('@')[0]}
defaultDomain = {this.props.defaultDomain}
notificationCenter = {this.props.notificationCenter}
/>
<ConferenceDrawer
show={this.state.showDrawer && !this.state.reconnectingCall}
close={this.toggleDrawer}
isLandscape={this.props.isLandscape}
title="Conference data"
>
<View style={this.props.isLandscape ? [{maxHeight: Dimensions.get('window').height - 60}, styles.landscapeDrawer] : styles.container}>
<View style={{flex: this.props.isLandscape ? 1 : 2}}>
<ConferenceDrawerSpeakerSelectionWrapper
selectSpeaker={this.startSpeakerSelection}
activeSpeakers={this.state.activeSpeakers}
/>
<ConferenceDrawerParticipantList style={styles.container}>
{drawerParticipants}
</ConferenceDrawerParticipantList>
</View>
</View>
</ConferenceDrawer>
<ConferenceDrawer
show={this.state.showSpeakerSelection}
close={this.toggleSpeakerSelection}
isLandscape={this.props.isLandscape}
showBackdrop={false}
title={`Select speaker ${this.selectSpeaker}`}
>
<ConferenceDrawerSpeakerSelection
participants={this.state.participants.concat([{id: this.props.call.id, publisherId: this.props.call.id, identity: this.props.call.localIdentity}])}
selected={this.handleActiveSpeakerSelected}
activeSpeakers={this.state.activeSpeakers}
selectSpeaker={this.selectSpeaker}
key = {this.state.activeSpeakers}
/>
</ConferenceDrawer>
</View>
);
}
}
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 = (
// <View>
// <Icon name="clock-outline" />{duration} - <Icon name="account-group" />{participantCount} participant{participantCount > 1 ? 's' : ''}
// </View>
// );
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 = (
<Appbar.Header style={{backgroundColor: 'rgba(34,34,34,.7)'}}>
<Appbar.Content
title={`Conference: ${room}`}
subtitle={callDetail}
/>
{props.audioOnly ? null : props.buttons.top.right}
</Appbar.Header>
);
callButtons = (
<View style={styles.buttonContainer}>
{props.buttons.bottom}
</View>
);
}
return (
<View style={styles.container}>
{videoHeader}
{callButtons}
</View>
);
}
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 (
<Portal>
<DialogType visible={this.props.show} onDismiss={this.onHide}>
<Surface style={styles.container}>
<Dialog.Title style={styles.title}>Join Conference</Dialog.Title>
<TextInput
style={styles.body}
mode="flat"
autoCapitalize="none"
label="Enter the room you wish to join"
placeholder="Conference room"
onChangeText={(text) => {this.handleConferenceTargetChange(text);}}
name="uri"
required
defaultValue={this.state.targetUri}
/>
<TextInput
style={styles.body}
mode="flat"
autoCapitalize="none"
name="users"
label="Invite people"
onChangeText={(value) => {this.setState({participants: value});}}
value={this.state.participants}
placeholder="Enter accounts separated by ,"
/>
<View style={styles.buttonRow}>
<Button
mode="contained"
style={styles.button}
onPress={this.joinAudio}
disabled={!validUri}
icon="speaker"
>Audio</Button>
<Button
mode="contained"
style={styles.button}
onPress={this.joinVideo}
disabled={!validUri}
icon="video"
>Video</Button>
</View>
</Surface>
</DialogType>
</Portal>
);
}
}
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 (
<Portal>
<DialogType visible={this.props.show} onDismiss={this.props.close}>
<Surface style={styles.container}>
<Dialog.Title style={styles.title}>{this.props.room}</Dialog.Title>
<Dialog.Content>
<Paragraph>People you wish to invite when you join the room:</Paragraph>
</Dialog.Content>
<TextInput
mode="flat"
- name="users"
+ name="participants"
label="People"
onChangeText={this.onInputChange}
- value={this.state.users}
+ value={this.state.participants}
placeholder="Enter accounts separated by ,"
required
autoCapitalize="none"
/>
<View style={styles.buttonRow}>
<Button
mode="contained"
style={styles.button}
onPress={this.saveParticipants}
icon="email">Save
</Button>
</View>
</Surface>
</DialogType>
</Portal>
);
}
}
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 }) => (
<View style={styles.participantView}>
{displayName !== uri?
<Text style={styles.participant}>{displayName} ({uri})</Text>
:
<Text style={styles.participant}>{uri}</Text>
}
</View>
);
const renderItem = ({ item }) => (
<Item nr={item.nr} uri={item.uri} displayName={item.displayName}/>
);
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 (
<Fragment>
<Card
onPress={() => {this.setTargetUri(uri, this.props.contact)}}
style={[containerClass, cardClass]}
>
<Card.Content style={styles.content}>
<View style={styles.mainContent}>
<Title noWrap style={color}>{title}</Title>
<Subheading noWrap style={color}>{subtitle}</Subheading>
<Caption color="textSecondary">
<Icon name={this.props.contact.direction == 'received' ? 'arrow-bottom-left' : 'arrow-top-right'}/>{description}
</Caption>
{participantsData && participantsData.length && showActions ?
<SafeAreaView>
<FlatList
horizontal={false}
data={participantsData}
renderItem={renderItem}
keyExtractor={item => item.id}
key={item => item.id}
/>
</SafeAreaView>
: null}
</View>
<View style={styles.userAvatarContent}>
<UserIcon style={styles.userIcon} identity={this.state}/>
</View>
</Card.Content>
{showActions ?
<View style={styles.buttonContainer}>
<Card.Actions>
{showEditButton? <Button mode={buttonMode} style={styles.button} onPress={() => {this.editConference()}}>{editTextbutton}</Button>: null}
{showDeleteButton? <Button mode={buttonMode} style={styles.button} onPress={() => {this.deleteHistoryEntry()}}>{deleteTextbutton}</Button>: null}
{showBlockButton? <Button mode={buttonMode} style={styles.button} onPress={() => {this.setBlockedUri()}}>{blockTextbutton}</Button>: null}
{showBlockDomainButton? <Button mode={buttonMode} style={styles.button} onPress={() => {this.setBlockedDomain()}}>{blockDomainTextbutton}</Button>: null}
{showFavoriteButton?<Button mode={buttonMode} style={styles.button} onPress={() => {this.setFavoriteUri()}}>{favoriteTextbutton}</Button>: null}
{showUndoButton?<Button mode={buttonMode} style={styles.button} onPress={() => {this.undo()}}>{undoTextbutton}</Button>: null}
</Card.Actions>
</View>
: null}
</Card>
<EditConferenceModal
show={this.state.showEditConferenceModal}
room={title}
invitedParties={this.state.invitedParties}
selectedContact={this.state.contact}
saveInvitedParties={this.saveInvitedParties}
close={this.toggleEditConferenceModal}
defaultDomain={this.props.defaultDomain}
+ accountId={this.props.accountId}
/>
</Fragment>
);
} else {
return (
<Card
onPress={() => {this.props.setTargetUri(uri, this.props.contact)}}
style={[containerClass, cardClass]}
>
<Card.Content style={styles.content}>
<View style={styles.mainContent}>
<Title noWrap style={color}>{title}</Title>
<Subheading noWrap style={color}>{uri}</Subheading>
<Caption color="textSecondary">
{this.state.label}
</Caption>
</View>
<View style={styles.userAvatarContent}>
<UserIcon style={styles.userIcon} identity={this.state}/>
</View>
</Card.Content>
{showActions ?
<View style={styles.buttonContainer}>
<Card.Actions>
{showBlockButton? <Button mode={buttonMode} style={styles.button} onPress={() => {this.setBlockedUri()}}>{blockTextbutton}</Button>: null}
{showFavoriteButton?<Button mode={buttonMode} style={styles.button} onPress={() => {this.setFavoriteUri()}}>{favoriteTextbutton}</Button>: null}
{showUndoButton?<Button mode={buttonMode} style={styles.button} onPress={() => {this.undo()}}>{undoTextbutton}</Button>: null}
</Card.Actions>
</View>
: null}
</Card>
);
}
}
}
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(
<HistoryCard
id={item.id}
contact={item.item}
filter={this.props.filter}
invitedParties={invitedParties}
setFavoriteUri={this.setFavoriteUri}
saveInvitedParties={this.saveInvitedParties}
setBlockedUri={this.setBlockedUri}
deleteHistoryEntry={this.deleteHistoryEntry}
setTargetUri={this.setTargetUri}
orientation={this.props.orientation}
isTablet={this.props.isTablet}
contacts={this.state.contacts}
defaultDomain={this.props.defaultDomain}
+ accountId={this.state.accountId}
/>);
}
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 (
<SafeAreaView style={styles.container}>
<FlatList
horizontal={false}
numColumns={columns}
onRefresh={this.getServerHistory}
refreshing={this.state.isRefreshing}
data={items}
renderItem={this.renderItem}
listKey={item => item.id}
key={this.props.orientation}
/>
</SafeAreaView>
);
}
}
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 (
<Fragment>
<CallOverlay
show = {true}
remoteUri={this.props.remoteUri}
remoteDisplayName={this.props.remoteDisplayName}
call = {this.props.call}
connection={this.props.connection}
+ terminated={this.props.terminated}
/>
{this.showSaveDialog() ?
<View style={styles.buttonContainer}>
<Text style={styles.title}>Save conference maybe?</Text>
<Text style={styles.subtitle}>Would you like to save participants {this.state.participants.toString().replace(/,/g, ', ')} for having another conference later?</Text>
<Text style={styles.description}>You can find later it in your Favorites. </Text>
<View style={styles.buttonRow}>
<Button
mode="contained"
style={styles.savebutton}
onPress={this.saveConference}
icon="content-save"
>Save</Button>
<Button
mode="contained"
style={styles.backbutton}
onPress={this.hangupCall}
icon=""
> Back</Button>
</View>
</View>
:
<View style={buttonContainerClass}>
<IconButton style={styles.hangupbutton} key="hangupButton" onPress={this.hangupCall} icon="phone-hangup" size={buttonSize} />
</View>
}
<View style={styles.container}>
<RTCView objectFit="cover"
style={[styles.video, videoStyle]}
id="localVideo"
ref={this.localVideo}
streamURL={streamUrl}
mirror={true}
/>
</View>
</Fragment>
);
}
}
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;

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 3:18 AM (20 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408677
Default Alt Text
(129 KB)

Event Timeline