diff --git a/app/app.js b/app/app.js
index fd0bd92..8169831 100644
--- a/app/app.js
+++ b/app/app.js
@@ -1,1277 +1,1287 @@
import React, { Component, Fragment } from 'react';
import { View, SafeAreaView, ImageBackground, PermissionsAndroid, AppState, Linking} from 'react-native';
import { Provider as PaperProvider, DefaultTheme } from 'react-native-paper';
import { BreadProvider } from "material-bread";
import { registerGlobals } from 'react-native-webrtc';
import { Router, Route, Link, Switch } from 'react-router-native';
import history from './history';
import Logger from "../Logger";
import DigestAuthRequest from 'digest-auth-request';
import autoBind from 'auto-bind';
import { firebase } from '@react-native-firebase/messaging';
import VoipPushNotification from 'react-native-voip-push-notification';
import uuid from 'react-native-uuid';
registerGlobals();
import * as sylkrtc from 'sylkrtc';
import InCallManager from 'react-native-incall-manager';
import RNCallKeep, { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep';
import RegisterBox from './components/RegisterBox';
import ReadyBox from './components/ReadyBox';
import Call from './components/Call';
import CallByUriBox from './components/CallByUriBox';
import Conference from './components/Conference';
import ConferenceByUriBox from './components/ConferenceByUriBox';
// import AudioPlayer from './components/AudioPlayer';
// import ErrorPanel from './components/ErrorPanel';
import FooterBox from './components/FooterBox';
import StatusBox from './components/StatusBox';
import IncomingCallModal from './components/IncomingCallModal';
import NotificationCenter from './components/NotificationCenter';
import LoadingScreen from './components/LoadingScreen';
import NavigationBar from './components/NavigationBar';
import Preview from './components/Preview';
import CallManager from "./CallManager";
import utils from './utils';
import config from './config';
import storage from './storage';
import styles from './assets/styles/blink/root.scss';
const backgroundImage = require('./assets/images/dark_linen.png');
const logger = new Logger("App");
const theme = {
...DefaultTheme,
dark: true,
roundness: 2,
colors: {
...DefaultTheme.colors,
primary: '#337ab7',
// accent: '#f1c40f',
},
};
const callkeepOptions = {
ios: {
appName: 'Sylk',
maximumCallGroups: 1,
maximumCallsPerCallGroup: 1,
supportsVideo: true,
imageName: "Image"
},
android: {
alertTitle: 'Calling Account Permission Required',
alertDescription: 'Please enable "Sylk" from your available Calling Accounts',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'phone_account_icon',
additionalPermissions: [PermissionsAndroid.PERMISSIONS.CAMERA, PermissionsAndroid.PERMISSIONS.RECORD_AUDIO ]
}
};
RNCallKeep.setup(callkeepOptions);
let callkeepType = Platform.OS === 'ios' ? 'generic' : 'sip';
// Application modes
const MODE_NORMAL = Symbol('mode-normal');
const MODE_PRIVATE = Symbol('mode-private');
const MODE_GUEST_CALL = Symbol('mode-guest-call');
const MODE_GUEST_CONFERENCE = Symbol('mode-guest-conference');
class Blink extends Component {
constructor() {
super();
autoBind(this)
this._loaded = false;
this._initialSstate = {
accountId: '',
password: '',
displayName: '',
account: null,
registrationState: null,
currentCall: null,
connection: null,
inboundCall: null,
showIncomingModal: false,
showScreenSharingModal: false,
status: null,
targetUri: '',
missedTargetUri: '',
loading: null,
mode: MODE_PRIVATE,
localMedia: null,
generatedVideoTrack: false,
history: [],
serverHistory: [],
devices: {},
pushtoken: null
};
this.state = Object.assign({}, this._initialSstate);
this.__notificationCenter = null;
this.participantsToInvite = null;
this.redirectTo = null;
this.prevPath = null;
this.shouldUseHashRouting = false;
this.muteIncoming = false;
storage.initialize();
RNCallKeep.addEventListener('checkReachability', () => {
RNCallKeep.setReachable();
});
this._callManager = new CallManager(RNCallKeep, this.answerCall, this.rejectCall, this.hangupCall);
// Load camera/mic preferences
storage.get('devices').then((devices) => {
if (devices) {
this.setState({devices: devices});
}
});
}
get _notificationCenter() {
// getter to lazy-load the NotificationCenter ref
if (!this.__notificationCenter) {
this.__notificationCenter = this.refs.notificationCenter;
}
return this.__notificationCenter;
}
async componentDidMount() {
this._loaded = true;
history.push('/login');
// prime the ref
logger.debug('NotificationCenter ref: %o', this._notificationCenter);
this._boundOnPushkitRegistered = this._onPushkitRegistered.bind(this);
if (Platform.OS === 'android') {
Linking.getInitialURL().then((url) => {
if (url) {
console.log('Initial url is: ' + url);
}
}).catch(err => {
logger.error({ err }, 'Error getting initial URL');
});
firebase.messaging().getToken()
.then(fcmToken => {
if (fcmToken) {
this._onPushkitRegistered(fcmToken);
}
});
}
if (Platform.OS === 'ios') {
VoipPushNotification.addEventListener('register', this._boundOnPushkitRegistered);
VoipPushNotification.registerVoipToken();
}
this.boundRnStartAction = this._callkeepStartedCall.bind(this);
RNCallKeep.addEventListener('didReceiveStartCallAction', this.boundRnStartAction);
AppState.addEventListener('change', this._handleAppStateChange);
if (Platform.OS === 'ios') {
this._boundOnNotificationReceivedBackground = this._onNotificationReceivedBackground.bind(this);
VoipPushNotification.addEventListener('notification', this._boundOnNotificationReceivedBackground);
}
if (Platform.OS === 'android') {
firebase
.messaging()
.requestPermission()
.then(() => {
// User has authorised
})
.catch(error => {
// User has rejected permissions
});
this.messageListener = firebase
.messaging()
.onMessage((message: RemoteMessage) => {
// Process your message as required
//on any message, register
});
}
}
_callkeepStartedCall(data) {
logger.debug('accessing Call Object', this._tmpCallStartInfo, data);
if (this._tmpCallStartInfo.options && this._tmpCallStartInfo.options.conference) {
this.startConference(data.handle);
} else if (this._tmpCallStartInfo.options) {
this.startCall(data.handle, this._tmpCallStartInfo.options);
} else {
// we dont have options in the tmp var, which means this likely came from the native dialer
// for now, we only do audio calls from the native dialer.
this._tmpCallStartInfo = {
uuid: data.callUUID
};
this.startCall(data.handle, {audio: true, video: false});
}
}
_onPushkitRegistered(token) {
logger.debug('pushkit token', token);
this.setState({ pushToken: token });
}
componentWillUnmount() {
RNCallKeep.removeEventListener('didReceiveStartCallAction', this.boundRnStartAction);
AppState.removeEventListener('change', this._handleAppStateChange);
}
_onNotificationReceivedBackground(notification) {
let notificationContent = notification.getData();
//console.log('got a pushkit call', notificationContent);
// get the uuid from the notification
// have we already got a waiting call in call manager? if we do, then its been "answered" and we're waiting for the invite
// we may still never get the invite if theres network issues... so still need a timeout
// no waiting call, so that means its still "ringing" (it may have been cancelled) so set a timer and if we havent recieved
// an invite within 10 seconds then clear it down
let callUUID = notificationContent.callUUID;
if (VoipPushNotification.wakeupByPush) {
VoipPushNotification.wakeupByPush = false;
}
}
_handleAppStateChange = nextAppState => {
//TODO - stop if we havent been backgrounded because of becoming active from a push notification and then going background again
if (Platform.OS === "ios") {
if (nextAppState.match(/background/)) {
logger.debug('app moving to background so we should stop the client sylk client if we dont have an active call');
}
}
if (nextAppState == "active") {
}
}
connectionStateChanged(oldState, newState) {
logger.debug(`Connection state changed! ${oldState} -> ${newState}`);
switch (newState) {
case 'closed':
this.setState({connection: null, loading: null});
break;
case 'ready':
this.processRegistration(this.state.accountId, this.state.password, this.state.displayName);
break;
case 'disconnected':
if (this.state.localMedia) {
sylkrtc.utils.closeMediaStream(this.state.localMedia);
}
if (this.state.currentCall) {
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
this.state.currentCall.terminate();
}
if (this.state.inboundCall && this.state.inboundCall !== this.state.currentCall) {
this.state.inboundCall.removeListener('stateChanged', this.inboundCallStateChanged);
this.state.inboundCall.terminate();
}
this.setState({
account:null,
registrationState: null,
loading: 'Disconnected, reconnecting...',
showIncomingModal: false,
currentCall: null,
inboundCall: null,
localMedia: null,
generatedVideoTrack: false
});
InCallManager.stop();
break;
default:
this.setState({loading: 'Connecting...'});
break;
}
}
notificationCenter() {
return this._notificationCenter;
}
registrationStateChanged(oldState, newState, data) {
logger.debug('Registration state changed! ' + newState);
this.setState({registrationState: newState});
if (newState === 'failed') {
let reason = data.reason;
if (reason.match(/904/)) {
// Sofia SIP: WAT
reason = 'Bad account or password';
} else {
reason = 'Connection failed';
}
this.setState({
loading : null,
status : {
msg : 'Sign In failed: ' + reason,
level : 'danger'
}
});
} else if (newState === 'registered') {
this.setState({loading: null});
this.getServerHistory();
RNCallKeep.setAvailable(true);
history.push('/ready');
return;
} else {
this.setState({status: null });
}
}
callStateChanged(oldState, newState, data) {
logger.debug(`Call state changed! ${oldState} -> ${newState}`. data);
switch (newState) {
case 'progress':
if (Platform.OS === 'ios') {
InCallManager.startRingback('_BUNDLE_');
} else {
InCallManager.startRingback('_DTMF_');
}
break;
case 'accepted':
this._callManager.callKeep.backToForeground();
InCallManager.stopRingback();
logger.debug('Setting Call as active in callkeep', this.state.currentCall._callkeepUUID);
this._callManager.callKeep.setCurrentCallActive(this.state.currentCall._callkeepUUID);
break;
case 'terminated':
InCallManager.stop({busytone: '_BUNDLE_'});
let callSuccesfull = false;
let reason = data.reason;
let CALLKEEP_REASON;
if (!reason || reason.match(/200/)) {
reason = 'Hangup';
callSuccesfull = true;
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED;
} else if (reason.match(/403/)) {
reason = 'This domain is not served here';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else if (reason.match(/404/)) {
reason = 'User not found';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else if (reason.match(/408/)) {
reason = 'Timeout';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else if (reason.match(/480/)) {
reason = 'User not online';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED;
} else if (reason.match(/486/) || reason.match(/60[036]/)) {
reason = 'Busy';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED;
} else if (reason.match(/487/)) {
reason = 'Cancelled';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED;
} else if (reason.match(/488/)) {
reason = 'Unacceptable media';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else if (reason.match(/5\d\d/)) {
reason = 'Server failure';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else if (reason.match(/904/)) {
// Sofia SIP: WAT
reason = 'Bad account or password';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
} else {
reason = 'Connection failed';
CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED;
}
this._callManager.callKeep.reportEndCallWithUUID(this.state.currentCall._callkeepUUID, CALLKEEP_REASON);
this._callManager.remove();
this._notificationCenter.postSystemNotification('Call ended', {body: reason, timeout: callSuccesfull ? 5 : 10});
this.setState({
currentCall : null,
targetUri : callSuccesfull || config.useServerCallHistory ? '' : this.state.targetUri,
showIncomingModal : false,
inboundCall : null,
localMedia : null,
generatedVideoTrack : false
});
this.setFocusEvents(false);
this.participantsToInvite = null;
history.push('/ready');
setTimeout(() => {
this.getServerHistory();
}, 1000);
break;
default:
break;
}
}
inboundCallStateChanged(oldState, newState, data) {
logger.debug('Inbound Call state changed! ' + newState);
if (newState === 'terminated') {
this.setState({ inboundCall: null, showIncomingModal: false });
this.setFocusEvents(false);
}
}
handleCallByUri(displayName, targetUri) {
const accountId = `${utils.generateUniqueId()}@${config.defaultGuestDomain}`;
this.setState({
accountId : accountId,
password : '',
displayName : displayName,
//mode : MODE_GUEST_CALL,
targetUri : utils.normalizeUri(targetUri, config.defaultDomain),
loading : 'Connecting...'
});
if (this.state.connection === null) {
let connection = sylkrtc.createConnection({server: config.wsServer});
connection.on('stateChanged', this.connectionStateChanged);
this.setState({connection: connection});
} else {
logger.debug('Connection Present, try to register');
this.processRegistration(accountId, '', displayName);
}
}
handleConferenceByUri(displayName, targetUri) {
const accountId = `${utils.generateUniqueId()}@${config.defaultGuestDomain}`;
this.setState({
accountId : accountId,
password : '',
displayName : displayName,
//mode : MODE_GUEST_CONFERENCE,
targetUri : targetUri,
loading : 'Connecting...'
});
if (this.state.connection === null) {
let connection = sylkrtc.createConnection({server: config.wsServer});
connection.on('stateChanged', this.connectionStateChanged);
this.setState({connection: connection});
} else {
logger.debug('Connection Present, try to register');
this.processRegistration(accountId, '', displayName);
}
}
handleRegistration(accountId, password, remember) {
this.setState({
accountId : accountId,
password : password,
mode : remember ? MODE_NORMAL : MODE_PRIVATE,
loading : 'Connecting...'
});
if (this.state.connection === null) {
let connection = sylkrtc.createConnection({server: config.wsServer});
connection.on('stateChanged', this.connectionStateChanged);
this.setState({connection: connection});
} else {
logger.debug('Connection Present, try to register');
this.processRegistration(accountId, password, '');
}
}
processRegistration(accountId, password, displayName) {
if (this.state.account !== null) {
logger.debug('We already have an account, removing it');
this.state.connection.removeAccount(this.state.account,
(error) => {
if (error) {
logger.debug(error);
}
this.setState({account: null, registrationState: null});
}
);
}
const options = {
account: accountId,
password: password,
displayName: displayName
};
const account = this.state.connection.addAccount(options, (error, account) => {
if (!error) {
account.on('outgoingCall', this.outgoingCall);
account.on('conferenceCall', this.outgoingCall);
switch (this.state.mode) {
case MODE_PRIVATE:
case MODE_NORMAL:
account.on('registrationStateChanged', this.registrationStateChanged);
account.on('incomingCall', this.incomingCall);
account.on('missedCall', this.missedCall);
account.on('conferenceInvite', this.conferenceInvite);
this.setState({account: account});
this.state.account.register();
if (this.state.mode !== MODE_PRIVATE) {
storage.set('account', {
accountId: this.state.accountId,
password: this.state.password
});
} else {
// Wipe storage if private login
//storage.remove('account'); // lets try this out
// history.clear().then(() => {
// this.setState({history: []});
// });
}
break;
case MODE_GUEST_CALL:
this.setState({account: account, loading: null, registrationState: 'registered'});
logger.debug(`${accountId} (guest) signed in`);
// Start the call immediately, this is call started with "Call by URI"
this.startGuestCall(this.state.targetUri, {audio: true, video: true});
break;
case MODE_GUEST_CONFERENCE:
this.setState({account: account, loading: null, registrationState: 'registered'});
logger.debug(`${accountId} (conference guest) signed in`);
// Start the call immediately, this is call started with "Conference by URI"
this.startGuestConference(this.state.targetUri);
break;
default:
logger.debug(`Unknown mode: ${this.state.mode}`);
break;
}
} else {
logger.debug('Add account error: ' + error);
this.setState({loading: null, status: {msg: error.message, level:'danger'}});
}
});
}
setDevice(device) {
const oldDevices = Object.assign({}, this.state.devices);
if (device.kind === 'videoinput') {
oldDevices['camera'] = device;
} else if (device.kind === 'audioinput') {
oldDevices['mic'] = device;
}
this.setState({devices: oldDevices});
storage.set('devices', oldDevices);
sylkrtc.utils.closeMediaStream(this.state.localMedia);
this.getLocalMedia();
}
getLocalMedia(mediaConstraints={audio: true, video: true}, nextRoute=null) { // eslint-disable-line space-infix-ops
logger.debug('getLocalMedia(), mediaConstraints=%o', mediaConstraints);
const constraints = Object.assign({}, mediaConstraints);
if (constraints.video === true) {
if ((nextRoute === '/conference' || this.state.mode === MODE_GUEST_CONFERENCE)) {
constraints.video = {
'width': {
'ideal': 640
},
'height': {
'ideal': 480
}
};
// TODO: remove this, workaround so at least safari works wehn joining a video conference
} else if ((nextRoute === '/conference' || this.state.mode === MODE_GUEST_CONFERENCE) && isSafari) {
constraints.video = false;
} else {
// ask for 720p video
constraints.video = {
'width': {
'ideal': 1280
},
'height': {
'ideal': 720
}
};
}
}
logger.debug('getLocalMedia(), (modified) mediaConstraints=%o', constraints);
this.loadScreenTimer = setTimeout(() => {
this.setState({loading: 'Please allow access to your media devices'});
}, 150);
navigator.mediaDevices.enumerateDevices()
.then((devices) => {
devices.forEach((device) => {
if ('video' in constraints && 'camera' in this.state.devices) {
if (constraints.video !== false && (device.deviceId === this.state.devices.camera.deviceId || device.label === this.state.devices.camera.label)) {
constraints.video.deviceId = {
exact: device.deviceId
};
}
}
if ('mic' in this.state.devices) {
if (device.deviceId === this.state.devices.mic.deviceId || device.label === this.state.devices.mic.Label) {
// constraints.audio = {
// deviceId: {
// exact: device.deviceId
// }
// };
}
}
});
})
.catch((error) => {
logger.debug('Device enumeration failed: %o', error);
})
.then(() => {
return navigator.mediaDevices.getUserMedia(constraints)
})
.then((localStream) => {
clearTimeout(this.loadScreenTimer);
logger.debug('Got local Media', localStream);
this.setState({status: null, loading: null, localMedia: localStream});
if (nextRoute !== null) {
history.push(nextRoute);
}
})
.catch((error) => {
logger.debug('Access failed, trying audio only: %o', error);
navigator.mediaDevices.getUserMedia({
audio: true,
video: false
})
.then((localStream) => {
clearTimeout(this.loadScreenTimer);
if (nextRoute != '/preview') {
logger.debug('Audio only media, but video was requested, creating generated video track');
const generatedVideoTrack = utils.generateVideoTrack(localStream);
localStream.addTrack(generatedVideoTrack);
}
this.setState({status: null, loading: null, localMedia: localStream, generatedVideoTrack: true});
if (nextRoute !== null) {
history.push(nextRoute);
}
})
.catch((error) => {
logger.debug('Access to local media failed: %o', error);
clearTimeout(this.loadScreenTimer);
this._notificationCenter.postSystemNotification("Can't access camera or microphone", {timeout: 10});
this.setState({
loading: null
});
});
});
}
callKeepStartCall(targetUri, options) {
this._tmpCallStartInfo = {
uuid: uuid.v4(),
options,
};
logger.debug('Set Call Object', this._tmpCallStartInfo);
this._callManager.callKeep.startCall(this._tmpCallStartInfo.uuid, targetUri, '', callkeepType, false);
}
startCall(targetUri, options) {
this.setState({targetUri: targetUri});
this.addCallHistoryEntry(targetUri);
this.getLocalMedia(Object.assign({audio: true, video: true}, options), '/call');
}
startGuestCall(targetUri, options) {
this.setState({targetUri: targetUri});
this.getLocalMedia(Object.assign({audio: true, video: true}, this._tmpCallStartInfo.options));
}
callKeepAnswerCall(options) {
if (this.state.currentCall) {
this._callManager.callKeep.answerIncomingCall(this.state.currentCall._callkeepUUID);
}
}
answerCall(options) {
this.setState({ showIncomingModal: false });
this.setFocusEvents(false);
if (this.state.inboundCall !== this.state.currentCall) {
// terminate current call to switch to incoming one
this.state.inboundCall.removeListener('stateChanged', this.inboundCallStateChanged);
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
//this.state.currentCall.terminate();
this._callManager.callKeep.endCall(this.state.currentCall._callkeepUUID);
this.setState({currentCall: this.state.inboundCall, inboundCall: this.state.inboundCall, localMedia: null});
this.state.inboundCall.on('stateChanged', this.callStateChanged);
}
this.getLocalMedia(Object.assign({audio: true, video: true}, options), '/call');
}
callKeepRejectCall() {
if (this.state.currentCall) {
this._callManager.callKeep.rejectCall(this.state.currentCall._callkeepUUID);
}
}
rejectCall() {
this.setState({showIncomingModal: false});
this.state.inboundCall.terminate();
}
callKeepHangupCall() {
if (this.state.currentCall) {
this._callManager.callKeep.endCall(this.state.currentCall._callkeepUUID);
}
}
hangupCall() {
if (this.state.currentCall != null) {
this.state.currentCall.terminate();
} else {
// We have no call but we still want to cancel
if (this.state.localMedia != null) {
sylkrtc.utils.closeMediaStream(this.state.localMedia);
}
history.push('/ready');
}
}
callKeepSendDtmf(digits) {
if (this.state.currentCall) {
this._callManager.callKeep.sendDTMF(this.state.currentCall._callkeepUUID, digits);
}
}
callKeepToggleMute(mute) {
if (this.state.currentCall) {
this._callManager.callKeep.setMutedCall(this.state.currentCall._callkeepUUID, mute);
}
}
escalateToConference(participants) {
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
this.state.currentCall.terminate();
history.push('/ready');
this.setState({currentCall: null, localMedia: null});
this.participantsToInvite = participants;
const uri = `${utils.generateSillyName()}@${config.defaultConferenceDomain}`;
- this.startConference(uri);
+ this.callKeepStartCall(uri, { conference: true });
}
startConference(targetUri) {
this.setState({targetUri: targetUri});
this.getLocalMedia({audio: true, video: true}, '/conference');
}
startGuestConference(targetUri) {
this.setState({targetUri: targetUri});
this.getLocalMedia({audio: true, video: true});
}
toggleMute() {
this.muteIncoming = !this.muteIncoming;
}
outgoingCall(call) {
this._callManager.handleSession(call, this._tmpCallStartInfo.uuid);
this._tmpCallStartInfo = {};
call.on('stateChanged', this.callStateChanged);
this.setState({currentCall: call});
InCallManager.start({media: false ? 'video' : 'audio'});
this._callManager.callKeep.updateDisplay(call._callkeepUUID, call.remoteIdentity.displayName, call.remoteIdentity.uri);
}
incomingCall(call, mediaTypes) {
this._callManager.handleSession(call);
logger.debug('New incoming call from %o with %o', call.remoteIdentity, mediaTypes);
if (!mediaTypes.audio && !mediaTypes.video) {
// call.terminate();
this.callKeepHangupCall();
return;
}
call.mediaTypes = mediaTypes;
if (this.state.currentCall !== null) {
// detect if we called ourselves
if (this.state.currentCall.localIdentity.uri === call.remoteIdentity.uri) {
logger.debug('Aborting call to myself');
//call.terminate();
this.callKeepHangupCall();
return;
}
InCallManager.start({media: mediaTypes.video ? 'video' : 'audio'});
RNCallKeep.displayIncomingCall(call._callkeepUUID, call.remoteIdentity.uri, call.remoteIdentity.displayName, callkeepType, mediaTypes.video);
this.setState({ showIncomingModal: true, inboundCall: call });
this.setFocusEvents(true);
call.on('stateChanged', this.inboundCallStateChanged);
} else {
if (!this.muteIncoming) {
//this.refs.audioPlayerInbound.play(true);
InCallManager.start({media: mediaTypes.video ? 'video' : 'audio'});
RNCallKeep.displayIncomingCall(call._callkeepUUID, call.remoteIdentity.uri, call.remoteIdentity.displayName, callkeepType, mediaTypes.video);
}
this.setFocusEvents(true);
call.on('stateChanged', this.callStateChanged);
this.setState({currentCall: call, inboundCall: call, showIncomingModal: true});
}
// if (!this.shouldUseHashRouting) {
// this._notificationCenter.postSystemNotification('Incoming call', {body: `From ${call.remoteIdentity.displayName || call.remoteIdentity.uri}`, timeout: 15, silent: false});
// }
}
setFocusEvents(enabled) {
// if (this.shouldUseHashRouting) {
// const remote = window.require('electron').remote;
// if (enabled) {
// const currentWindow = remote.getCurrentWindow();
// currentWindow.on('focus', this.hasFocus);
// currentWindow.on('blur', this.hasNoFocus);
// this.setState({haveFocus: currentWindow.isFocused()});
// } else {
// const currentWindow = remote.getCurrentWindow();
// currentWindow.removeListener('focus', this.hasFocus);
// currentWindow.removeListener('blur', this.hasNoFocus);
// }
// }
}
// hasFocus() {
// this.setState({haveFocus: true});
// }
// hasNoFocus() {
// this.setState({haveFocus: false});
// }
missedCall(data) {
logger.debug('Missed call from ' + data.originator);
this._notificationCenter.postSystemNotification('Missed call', {body: `From ${data.originator.displayName || data.originator.uri}`, timeout: 15, silent: false});
if (this.state.currentCall !== null || !config.useServerCallHistory) {
this._notificationCenter.postMissedCall(data.originator, () => {
if (this.state.currentCall !== null) {
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
//this.state.currentCall.terminate();
this.callKeepHangupCall();
this.setState({currentCall: null, missedTargetUri: data.originator.uri, showIncomingModal: false, localMedia: null});
} else {
this.setState({missedTargetUri: data.originator.uri});
}
history.push('/ready');
});
} else {
this.getServerHistory();
}
}
conferenceInvite(data) {
logger.debug('Conference invite from %o to %s', data.originator, data.room);
this._notificationCenter.postSystemNotification('Conference invite', {body: `From ${data.originator.displayName || data.originator.uri} for room ${data.room}`, timeout: 15, silent: false});
this._notificationCenter.postConferenceInvite(data.originator, data.room, () => {
if (this.state.currentCall !== null) {
this.state.currentCall.removeListener('stateChanged', this.callStateChanged);
this.state.currentCall.terminate();
this.setState({currentCall: null, showIncomingModal: false, localMedia: null, generatedVideoTrack: false});
}
- this.startConference(data.room);
+ this.callKeepStartCall(data.room, {conference: true});
});
}
startPreview() {
this.getLocalMedia({audio: true, video: true}, '/preview');
}
addCallHistoryEntry(uri) {
if (this.state.mode === MODE_NORMAL) {
// history.add(uri).then((entries) => {
// this.setState({history: entries});
// });
} else {
let entries = this.state.history.slice();
if (entries.length !== 0) {
const idx = entries.indexOf(uri);
if (idx !== -1) {
entries.splice(idx, 1);
}
entries.unshift(uri);
// keep just the last 50
entries = entries.slice(0, 50);
} else {
entries = [uri];
}
this.setState({history: entries});
}
}
getServerHistory() {
if (!config.useServerCallHistory) {
return;
}
logger.debug('Requesting call history from server');
let getServerCallHistory = new DigestAuthRequest(
'GET',
`${config.serverCallHistoryUrl}?action=get_history&realm=${this.state.account.id.split('@')[1]}`,
this.state.account.id.split('@')[0],
this.state.password
);
// Disable logging
getServerCallHistory.loggingOn = false;
getServerCallHistory.request((data) => {
if (data.success !== undefined && data.success === false) {
logger.debug('Error getting call history from server: %o', data.error_message)
return;
}
- let history = []
- data.placed.map(elem => {elem.direction = 'placed'; return elem});
- data.received.map(elem => {elem.direction = 'received'; return elem});
+ let history = [];
+ if (data.placed) {
+ data.placed.map(elem => {elem.direction = 'placed'; return elem});
+ }
+ if (data.received) {
+ data.received.map(elem => {elem.direction = 'received'; return elem});
+ }
history = data.placed;
- history = history.concat(data.received);
+ if (data.received) {
+ history = history.concat(data.received);
+ }
history.sort((a,b) => {
return new Date(b.startTime) - new Date(a.startTime);
});
const known = [];
history = history.filter((elem) => {
if (known.indexOf(elem.remoteParty) <= -1) {
if ((elem.media.indexOf('audio') > -1 || elem.media.indexOf('video') > -1) &&
(elem.remoteParty !== this.state.account.id || elem.direction !== 'placed')) {
known.push(elem.remoteParty);
return elem;
}
}
});
this.setState({serverHistory: history});
}, (errorCode) => {
logger.debug('Error getting call history from server: %o', errorCode)
});
}
// checkRoute(nextPath, navigation, match) {
// if (nextPath !== this.prevPath) {
// logger.debug(`Transition from ${this.prevPath} to ${nextPath}`);
//
// // Press back in ready after a login, prevent initial navigation
// // don't deny if there is no registrationState (connection fail)
// if (this.prevPath === '/ready' && nextPath === '/login' && this.state.registrationState !== null) {
// logger.debug('Transition denied redirecting to /logout');
// history.push('/logout');
// return false;
// // Press back in ready after a call
// } else if ((nextPath === '/call' || nextPath === '/conference') && this.state.localMedia === null && this.state.registrationState === 'registered') {
// return false;
// // Press back from within a call/conference, don't navigate terminate the call and
// // let termination take care of navigating
// } else if (nextPath === '/ready' && this.state.registrationState === 'registered' && this.state.currentCall !== null) {
// this.state.currentCall.terminate();
// return false;
// // Guest call ended, needed to logout and display msg and logout
// } else if (nextPath === '/ready' && (this.state.mode === MODE_GUEST_CALL || this.state.mode === MODE_GUEST_CONFERENCE)) {
// history.push('/logout');
// this.forceUpdate();
// }
// }
// this.prevPath = nextPath;
// }
render() {
let footerBox = ;
let extraStyles = {};
if (this.state.localMedia || this.state.registrationState === 'registered') {
footerBox = null;
}
return (
{/* */}
{/* */}
{/* */}
+
+
{footerBox}
);
}
notFound(match) {
const status = {
title : '404',
message : 'Oops, the page your looking for can\'t found',
level : 'danger',
width : 'large'
}
return (
);
}
ready() {
if (this.state.registrationState !== 'registered') {
history.push('/login');
return false;
};
+ InCallManager.stop();
+
return (
);
}
preview() {
if (this.state.registrationState !== 'registered') {
history.push('/login');
return false;
};
return (
);
}
call() {
if (this.state.registrationState !== 'registered') {
history.push('/login');
return false;
};
return (
)
}
callByUri(urlParameters) {
// check if the uri contains a domain
if (urlParameters.targetUri.indexOf('@') === -1) {
const status = {
title : 'Invalid user',
message : `Oops, the domain of the user is not set in '${urlParameters.targetUri}'`,
level : 'danger',
width : 'large'
}
return (
);
}
return (
);
}
conference() {
if (this.state.registrationState !== 'registered') {
history.push('/login');
return false;
};
return (
)
}
conferenceByUri(urlParameters) {
const targetUri = utils.normalizeUri(urlParameters.targetUri, config.defaultConferenceDomain);
const idx = targetUri.indexOf('@');
const uri = {};
const pattern = /^[A-Za-z0-9\-\_]+$/g;
uri.user = targetUri.substring(0, idx);
// check if the uri.user is valid
if (!pattern.test(uri.user)) {
const status = {
title : 'Invalid conference',
message : `Oops, the conference ID is invalid: ${targetUri}`,
level : 'danger',
width : 'large'
}
return (
);
}
return (
);
}
login() {
let registerBox;
let statusBox;
if (this.state.status !== null) {
statusBox = (
);
}
if (this.state.registrationState !== 'registered') {
registerBox = (
);
}
return (
{registerBox}
{statusBox}
);
}
logout() {
if (this.state.registrationState !== null && (this.state.mode === MODE_NORMAL || this.state.mode === MODE_PRIVATE)) {
this.state.account.unregister();
}
if (this.state.account !== null) {
this.state.connection.removeAccount(this.state.account, (error) => {
if (error) {
logger.debug(error);
}
});
}
storage.set('account', {accountId: this.state.accountId, password: ''});
this.setState({account: null, registrationState: null, status: null});
history.push('/login');
return null;
}
main() {
return null;
}
}
export default Blink;
\ No newline at end of file
diff --git a/app/assets/styles/blink/_EscalateConferenceModal.scss b/app/assets/styles/blink/_EscalateConferenceModal.scss
index 595bbbf..3521125 100644
--- a/app/assets/styles/blink/_EscalateConferenceModal.scss
+++ b/app/assets/styles/blink/_EscalateConferenceModal.scss
@@ -1,3 +1,16 @@
.container {
+ padding: 30px;
+ margin: 30px;
+}
-}
\ No newline at end of file
+.title {
+ padding: 5px;
+ font-size: 20px;
+ text-align: center;
+}
+
+.body {
+ padding: 10px;
+ font-size: 16px;
+ text-align: center;
+}
diff --git a/app/assets/styles/blink/_InviteParticipantsModal.scss b/app/assets/styles/blink/_InviteParticipantsModal.scss
new file mode 100644
index 0000000..3521125
--- /dev/null
+++ b/app/assets/styles/blink/_InviteParticipantsModal.scss
@@ -0,0 +1,16 @@
+.container {
+ padding: 30px;
+ margin: 30px;
+}
+
+.title {
+ padding: 5px;
+ font-size: 20px;
+ text-align: center;
+}
+
+.body {
+ padding: 10px;
+ font-size: 16px;
+ text-align: center;
+}
diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js
index 860641c..d5e56be 100644
--- a/app/components/ConferenceBox.js
+++ b/app/components/ConferenceBox.js
@@ -1,687 +1,684 @@
'use strict';
import React, {Component, Fragment} from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';
import * as sylkrtc from '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 ConferenceHeader from './ConferenceHeader';
import ConferenceCarousel from './ConferenceCarousel';
import ConferenceParticipant from './ConferenceParticipant';
import ConferenceMatrixParticipant from './ConferenceMatrixParticipant';
import ConferenceParticipantSelf from './ConferenceParticipantSelf';
import InviteParticipantsModal from './InviteParticipantsModal';
import styles from '../assets/styles/blink/_ConferenceBox.scss';
const DEBUG = debug('blinkrtc:ConferenceBox');
debug.enable('*');
class ConferenceBox extends Component {
constructor(props) {
super(props);
autoBind(this);
this.state = {
callOverlayVisible: true,
audioMuted: false,
videoMuted: false,
participants: props.call.participants.slice(),
showInviteModal: false,
showDrawer: false,
showFiles: false,
shareOverlayVisible: false,
activeSpeakers: props.call.activeParticipants.slice(),
selfDisplayedLarge: false,
eventLog: [],
sharedFiles: props.call.sharedFiles.slice(),
largeVideoStream: null
};
const friendlyName = this.props.remoteIdentity.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 = [];
[
'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});
}
);
});
}
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);
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.changeResolution();
}
if (this.props.call.getLocalStreams()[0].getVideoTracks().length !== 0) {
this.haveVideo = true;
}
}
componentWillUnmount() {
clearTimeout(this.overlayTimer);
this.uploads.forEach((upload) => {
this.props.notificationCenter().removeNotification(upload[1]);
upload[0].abort();
})
}
onParticipantJoined(p) {
DEBUG(`Participant joined: ${p.identity}`);
// this.refs.audioPlayerParticipantJoined.play();
p.on('stateChanged', this.onParticipantStateChanged);
p.attach();
this.setState({
participants: this.state.participants.concat([p])
});
// this.changeResolution();
}
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();
}
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.slice();
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);
}
})
}
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 = '';
}
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.remoteIdentity}/${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.remoteIdentity}/${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();
const localStream = this.props.call.getLocalStreams()[0];
if (localStream.getAudioTracks().length > 0) {
const track = localStream.getAudioTracks()[0];
if(this.state.audioMuted) {
DEBUG('Unmute microphone');
track.enabled = true;
this.setState({audioMuted: false});
} else {
DEBUG('Mute microphone');
track.enabled = false;
this.setState({audioMuted: true});
}
}
}
muteVideo(event) {
event.preventDefault();
const localStream = this.props.call.getLocalStreams()[0];
if (localStream.getVideoTracks().length > 0) {
const track = localStream.getVideoTracks()[0];
if (this.state.videoMuted) {
DEBUG('Unmute camera');
track.enabled = true;
this.setState({videoMuted: false});
} else {
DEBUG('Mute camera');
track.enabled = false;
this.setState({videoMuted: true});
}
}
}
hangup(event) {
event.preventDefault();
for (let participant of this.state.participants) {
participant.detach();
}
this.props.hangup();
}
armOverlayTimer() {
clearTimeout(this.overlayTimer);
this.overlayTimer = setTimeout(() => {
this.setState({callOverlayVisible: false});
}, 4000);
}
showOverlay() {
// if (!this.state.shareOverlayVisible && !this.state.showDrawer && !this.state.showFiles) {
// if (!this.state.callOverlayVisible) {
// this.setState({callOverlayVisible: true});
// }
// this.armOverlayTimer();
// }
}
toggleInviteModal() {
- // this.setState({showInviteModal: !this.state.showInviteModal});
- // if (this.refs.showOverlay) {
- // this.refs.shareOverlay.hide();
- // }
+ this.setState({showInviteModal: !this.state.showInviteModal});
}
toggleDrawer() {
this.setState({callOverlayVisible: true, showDrawer: !this.state.showDrawer, showFiles: 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);
}
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 remoteIdentity = this.props.remoteIdentity.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 bottomButtons = [];
bottomButtons.push(
);
bottomButtons.push(
);
bottomButtons.push(
);
// bottomButtons.push(
//
//
//
// );
bottomButtons.push(
);
buttons.bottom = bottomButtons;
const participants = [];
if (this.state.participants.length > 0) {
if (this.state.activeSpeakers.findIndex((element) => {return element.id === this.props.call.id}) === -1) {
participants.push(
);
}
}
const drawerParticipants = [];
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(
);
}
drawerParticipants.push(
);
});
} else {
this.state.participants.forEach((p) => {
videos.push(
);
drawerParticipants.push(
);
});
}
}
let filesDrawerContent = (
);
return (
{videos}
{participants}
{drawerParticipants}
);
}
}
ConferenceBox.propTypes = {
notificationCenter : PropTypes.func.isRequired,
call : PropTypes.object,
hangup : PropTypes.func,
remoteIdentity : PropTypes.string,
generatedVideoTrack : PropTypes.bool
};
export default ConferenceBox;
diff --git a/app/components/ConferenceDrawerLog.js b/app/components/ConferenceDrawerLog.js
index e1306f8..5869bc0 100644
--- a/app/components/ConferenceDrawerLog.js
+++ b/app/components/ConferenceDrawerLog.js
@@ -1,45 +1,45 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import utils from '../utils';
-import { Title } from 'react-native-paper';
+import { Title, Text, List, Avatar } from 'react-native-paper';
const ConferenceDrawerLog = (props) => {
const entries = props.log.map((elem, idx) => {
// const classes = classNames({
// 'text-danger' : elem.level === 'error',
// 'text-warning' : elem.level === 'warning',
// 'log-entry' : true
// });
-
+ console.log(elem)
const originator = elem.originator.displayName || elem.originator.uri || elem.originator;
const messages = elem.messages.map((message, index) => {
- return {message}
;
+ return {message};
});
+ const number = props.log.length - idx;
+
const color = utils.generateMaterialColor(elem.originator.uri || elem.originator)['300'];
- return null;
- // return (
- //
- // <{props.log.length - idx}
- //
- // {originator} {elem.action}
{messages}
- //
- // )
+ return (
+ }
+ />
+ )
});
return (
Configuration Events
{entries}
);
};
ConferenceDrawerLog.propTypes = {
log: PropTypes.array.isRequired
};
export default ConferenceDrawerLog;
diff --git a/app/components/ConferenceDrawerParticipant.js b/app/components/ConferenceDrawerParticipant.js
index a0cb7d5..882da61 100644
--- a/app/components/ConferenceDrawerParticipant.js
+++ b/app/components/ConferenceDrawerParticipant.js
@@ -1,38 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import UserIcon from './UserIcon';
+import { List, Text } from 'react-native-paper';
const ConferenceDrawerParticipant = (props) => {
+ let tag = null
+ if (props.isLocal) {
+ tag = 'Myself';
+ }
+ let participant = props.participant;
+
+ if (!participant) {
+ return null;
+ }
+
+ return (
+ }
+ right={props => tag ? {tag} : null}
+ />
+ )
- return null;
-
- // let tag = null
- // if (props.isLocal) {
- // tag = ;
- // }
-
- // return (
- //
- //
- //
- //
- //
- // {props.participant.identity.displayName || props.participant.identity.uri}
- //
- //
- // {tag}
- //
- //
- // );
}
ConferenceDrawerParticipant.propTypes = {
participant: PropTypes.object.isRequired,
isLocal: PropTypes.bool
};
export default ConferenceDrawerParticipant;
diff --git a/app/components/ConferenceDrawerParticipantList.js b/app/components/ConferenceDrawerParticipantList.js
index 69a3c16..9e76f9a 100644
--- a/app/components/ConferenceDrawerParticipantList.js
+++ b/app/components/ConferenceDrawerParticipantList.js
@@ -1,27 +1,21 @@
-import React from 'react';
-import { View } from 'react-native';
+import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
-import { List } from 'react-native-paper';
+import { List, Title } from 'react-native-paper';
const ConferenceDrawerParticipantList = (props) => {
- const items = [];
- let idx = 0;
- React.Children.forEach(props.children, (child) => {
- items.push();
- idx++;
- });
-
return (
-
- Participants
- {items}
-
+
+ Participants
+
+ {props.children}
+
+
);
};
ConferenceDrawerParticipantList.propTypes = {
children: PropTypes.node
};
export default ConferenceDrawerParticipantList;
diff --git a/app/components/ConferenceParticipant.js b/app/components/ConferenceParticipant.js
index 659e147..f5d1e29 100644
--- a/app/components/ConferenceParticipant.js
+++ b/app/components/ConferenceParticipant.js
@@ -1,133 +1,134 @@
import React from 'react';
import PropTypes from 'prop-types';
// const hark = require('hark');
+import { View } from 'react-native';
import classNames from 'classnames';
import autoBind from 'auto-bind';
import { IconButton } from 'react-native-paper';
import { RTCView } from 'react-native-webrtc';
import styles from '../assets/styles/blink/_ConferenceParticipant.scss';
class ConferenceParticipant extends React.Component {
constructor(props) {
super(props);
autoBind(this);
this.state = {
active: false,
hasVideo: false,
overlayVisible: false,
audioMuted: false,
stream: null
}
this.speechEvents = null;
this.videoElement = React.createRef();
props.participant.on('stateChanged', this.onParticipantStateChanged);
}
componentDidMount() {
this.maybeAttachStream();
// this.videoElement.current.oncontextmenu = (e) => {
// // disable right click for video elements
// e.preventDefault();
// };
}
componentWillUnmount() {
- this.videoElement.current.pause();
+ //this.videoElement.current.pause();
this.props.participant.removeListener('stateChanged', this.onParticipantStateChanged);
if (this.speechEvents !== null) {
this.speechEvents.stop();
this.speechEvents = null;
}
}
onParticipantStateChanged(oldState, newState) {
if (newState === 'established') {
this.maybeAttachStream();
}
}
onMuteAudioClicked(event) {
event.preventDefault();
const streams = this.props.participant.streams;
if (streams[0].getAudioTracks().length > 0) {
const track = streams[0].getAudioTracks()[0];
if(this.state.audioMuted) {
track.enabled = true;
this.setState({audioMuted: false});
} else {
track.enabled = false;
this.setState({audioMuted: true});
}
}
}
maybeAttachStream() {
const streams = this.props.participant.streams;
if (streams.length > 0) {
this.setState({stream: streams[0], hasVideo: streams[0].getVideoTracks().length > 0});
// const options = {
// interval: 150,
// play: false
// };
// this.speechEvents = hark(streams[0], options);
// this.speechEvents.on('speaking', () => {
// this.setState({active: true});
// });
// this.speechEvents.on('stopped_speaking', () => {
// this.setState({active: false});
// });
}
}
showOverlay() {
this.setState({overlayVisible: true});
}
hideOverlay() {
if (!this.state.audioMuted) {
this.setState({overlayVisible: false});
}
}
render() {
// const tooltip = (
// {this.props.participant.identity.displayName || this.props.participant.identity.uri}
// );
const classes = classNames({
'poster' : !this.state.hasVideo,
'conference-active' : this.state.active
});
let muteButton;
if (this.state.overlayVisible) {
const muteButtonIcons = this.state.audioMuted ? 'microphone-off' : 'microphone';
muteButton = (
);
}
return (
{muteButton}
{/* */}
{/* */}
);
}
}
ConferenceParticipant.propTypes = {
participant: PropTypes.object.isRequired
};
export default ConferenceParticipant;
diff --git a/app/components/EscalateConferenceModal.js b/app/components/EscalateConferenceModal.js
index 1d44618..853fb6c 100644
--- a/app/components/EscalateConferenceModal.js
+++ b/app/components/EscalateConferenceModal.js
@@ -1,65 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
+import autoBind from 'auto-bind';
import { Title, Portal, Modal, Paragraph, TextInput, Surface, Button } from 'react-native-paper';
import styles from '../assets/styles/blink/_EscalateConferenceModal.scss';
import config from '../config';
class EscalateConferenceModal extends React.Component {
constructor(props) {
super(props);
- this.invitees = React.createRef();
-
- this.escalate = this.escalate.bind(this);
+ autoBind(this);
+ this.state = {
+ users: null
+ }
}
escalate(event) {
event.preventDefault();
const uris = [];
- for (let item of this.invitees.current.value.split(',')) {
- item = item.trim();
- if (item.indexOf('@') === -1) {
- item = `${item}@${config.defaultDomain}`;
- }
- uris.push(item);
- };
+ if (this.state.users) {
+ for (let item of this.state.users.split(',')) {
+ item = item.trim();
+ if (item.indexOf('@') === -1) {
+ item = `${item}@${config.defaultDomain}`;
+ }
+ uris.push(item);
+ };
+ }
uris.push(this.props.call.remoteIdentity.uri);
this.props.escalateToConference(uris);
}
+ onInputChange(value) {
+ this.setState({users: value});
+ }
+
render() {
return (
Move to conference
Please enter the account(s) you wish to add to this call. After pressing Move, all parties will be invited to join a conference.
);
}
}
EscalateConferenceModal.propTypes = {
show: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
call: PropTypes.object,
escalateToConference: PropTypes.func
};
export default EscalateConferenceModal;
diff --git a/app/components/InviteParticipantsModal.js b/app/components/InviteParticipantsModal.js
index 66841de..c27ac24 100644
--- a/app/components/InviteParticipantsModal.js
+++ b/app/components/InviteParticipantsModal.js
@@ -1,61 +1,72 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { View } from 'react-native';
+import autoBind from 'auto-bind';
import { Modal, Portal, Text, Button, Surface, TextInput, Title } from 'react-native-paper';
import config from '../config';
+import styles from '../assets/styles/blink/_InviteParticipantsModal.scss';
class InviteParticipantsModal extends Component {
constructor(props) {
super(props);
- this.invitees = React.createRef();
-
- this.invite = this.invite.bind(this);
+ autoBind(this);
+ this.state = {
+ users: null
+ }
}
invite(event) {
event.preventDefault();
const uris = [];
- this.invitees.current.value.split(',').forEach((item) => {
- item = item.trim();
- if (item.indexOf('@') === -1) {
- item = `${item}@${config.defaultDomain}`;
- }
- uris.push(item);
- });
+ if (this.state.users) {
+ this.state.users.split(',').forEach((item) => {
+ item = item.trim();
+ if (item.indexOf('@') === -1) {
+ item = `${item}@${config.defaultDomain}`;
+ }
+ uris.push(item);
+ });
+ }
if (uris && this.props.call) {
this.props.call.inviteParticipants(uris);
}
this.props.close();
}
+ onInputChange(value) {
+ this.setState({users: value});
+ }
+
render() {
return (
-
+
Invite Online Users
Enter the users you wish to invite
-
+
);
}
}
InviteParticipantsModal.propTypes = {
show: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
call: PropTypes.object
};
export default InviteParticipantsModal;
diff --git a/app/components/NotificationCenter.js b/app/components/NotificationCenter.js
index 240def4..984b712 100644
--- a/app/components/NotificationCenter.js
+++ b/app/components/NotificationCenter.js
@@ -1,159 +1,168 @@
import React, { Component } from 'react';
import { ProgressBar, Colors, Snackbar } from 'react-native-paper';
import moment from 'moment';
import autoBind from 'auto-bind';
import styles from '../assets/styles/blink/_StatusBox.scss';
import config from '../config';
class NotificationCenter extends Component {
constructor(props) {
super(props);
autoBind(this);
this.state = {
visible: false,
message: null,
title: null,
autoDismiss: null,
action: null
}
}
+ componentDidMount() {
+ console.log('Notification Center mounted');
+ }
+
+ componentWillUnmount() {
+ console.log('Notification Center will unmount');
+ }
+
postSystemNotification(title, options={}) { // eslint-disable-line space-infix-ops
this.setState({
visible: true,
autoDismiss: 10,
title: title,
message: options.body
});
}
postConferenceInvite(originator, room, cb) {
- if (originator.uri.endsWith(config.defaultGuestDomain)) {
- return;
- }
+ // if (originator.uri.endsWith(config.defaultGuestDomain)) {
+ // return;
+ // }
const idx = room.indexOf('@');
if (idx === -1) {
return;
}
const currentDate = moment().format('MMMM Do YYYY [at] HH:mm:ss');
const action = {
label: 'Join',
onPress: () => { cb(room); }
};
this.setState({
visible: true,
- message: `${(originator.displayName || originator.uri)} invited you to join conference room ${room.substring(0, idx)}
On ${currentDate}`,
+ message: `${(originator.displayName || originator.uri)} invited you to join conference room ${room.substring(0, idx)} on ${currentDate}`,
title: 'Conference Invite',
- autoDismiss: 0,
+ autoDismiss: 20,
action: action,
});
}
postMissedCall(originator, cb) {
const currentDate = moment().format('MMMM Do YYYY [at] HH:mm:ss');
let action;
if (originator.uri.endsWith(config.defaultGuestDomain)) {
action = null;
} else {
action = {
label: 'Call',
onPress: () => { cb(originator.uri); }
};
}
this.setState({
visible: true,
message: `From ${(originator.displayName || originator.uri)}
On ${currentDate}`,
title: 'Missed Call',
autoDismiss: 0,
action: action
});
}
postFileUploadProgress(filename, cb) {
this.setState({
visible: true,
message: `${filename}`,
title: 'Uploading file',
autoDismiss: 0,
action: {
label: 'OK',
onPress: () => cb()
},
// children: (
//
//
//
// )
});
}
editFileUploadNotification(progress, notification) {
if (progress === undefined) {
progress = 100;
}
this.setState({
visible: true,
message: `${filename}`,
title: 'Upload Successful',
autoDismiss: 3,
});
}
removeFileUploadNotification(notification) {
let timer = setTimeout(() => {
this.setState({visible: false});
}, 3000);
}
removeNotification(notification) {
this.setState({visible: false});
}
postFileUploadFailed(filename) {
this.setState({
visible: true,
message: `Uploading of ${filename} failed`,
title: 'File sharing failed',
autoDismiss: 10,
});
}
postFileShared(file, cb) {
const uploader = file.uploader.displayName || file.uploader.uri || file.uploader;
this.setState({
visible: true,
message: `${uploader} shared ${file.filename}`,
title: 'File shared',
autoDismiss: 10,
action: {
label: 'Show Files',
onPress: () => cb()
}
});
}
render() {
+ console.log('showing snackbar');
return (
this.setState({ visible: false })}
+ onDismiss={() => this.setState({ visible: false, message: null, title: null })}
action={this.state.action}
>
{this.state.title} - {this.state.message}
);
}
}
export default NotificationCenter;
\ No newline at end of file