diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js index 2aa0189..ac842d3 100644 --- a/app/components/ConferenceBox.js +++ b/app/components/ConferenceBox.js @@ -1,2850 +1,2850 @@ 'use strict'; import React, {useState, Component, Fragment} from 'react'; import { Clipboard, View, Platform, TouchableWithoutFeedback, TouchableOpacity, Dimensions, SafeAreaView, ScrollView, FlatList, TouchableHighlight, Keyboard, Switch, Animated, PanResponder} 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, Text } from 'react-native-paper'; import uuid from 'react-native-uuid'; 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 { GiftedChat, Bubble, MessageText, Send, MessageImage } from 'react-native-gifted-chat' import {renderBubble } from './ContactsListBox'; import {launchCamera, launchImageLibrary} from 'react-native-image-picker'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import DocumentPicker from 'react-native-document-picker'; import RNFetchBlob from "rn-fetch-blob"; import VideoPlayer from 'react-native-video-player'; import xss from 'xss'; import * as RNFS from 'react-native-fs'; import styles from '../assets/styles/blink/_ConferenceBox.scss'; import RNBackgroundDownloader from 'react-native-background-downloader'; import md5 from "react-native-md5"; import FileViewer from 'react-native-file-viewer'; import _ from 'lodash'; import { produce } from "immer" import moment from 'moment'; import {StatusBar} from 'react-native'; 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.sliderTimeout = null; this.downloadRequests = {}; this.audioBytesReceived = new Map(); this.audioBandwidth = new Map(); this.bandwidthDownload = 0; this.bandwidthUpload = 0; this.videoBytesReceived = new Map(); this.videoBandwidth = new Map(); this.audioPacketLoss = new Map(); this.videoPacketLoss = new Map(); this.packetLoss = new Map(); this.latency = new Map(); this.mediaLost = new Map(); this.sampleInterval = 1; this.typingTimer = null; let renderMessages = []; if (this.props.remoteUri in this.props.messages) { renderMessages = this.props.messages[this.props.remoteUri]; } this.audioViewMinHeight = 170; let duration = 0; if (this.props.call) { let giftedChatMessage; let direction; duration = Math.floor((new Date() - this.props.callState.startTime) / 1000); this.props.call.messages.forEach((sylkMessage) => { if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } if (sylkMessage.type === 'status') { return; } const existingMessages = renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } direction = sylkMessage.state === 'received' ? 'incoming': 'outgoing'; if (direction === 'incoming' && sylkMessage.sender.uri === this.props.account.id) { direction = 'outgoing'; } - giftedChatMessage = utils.sylkToRenderMessage(sylkMessage, null, direction); + giftedChatMessage = utils.sylk2GiftedChat(sylkMessage, null, direction); renderMessages.push(giftedChatMessage); this.saveConferenceMessage(this.props.remoteUri, giftedChatMessage); }); } const videoEnabled = this.props.call && this.props.call.getLocalStreams()[0].getVideoTracks().length > 0; let bottomHeight = Dimensions.get('window').height * 50/100; //console.log('bottomHeight', bottomHeight); let participants = []; if (props.call) { props.call.participants.forEach((p) => { if (!p.timestamp) { p.timestamp = Date.now(); } }); participants = props.call.participants.slice(); } this.state = { callOverlayVisible: true, remoteUri: this.props.remoteUri, call: this.props.call, accountId: this.props.call ? this.props.call.account.id : null, renderMessages: renderMessages, ended: false, duration: duration, isTyping: false, keyboardVisible: false, videoEnabled: videoEnabled, audioMuted: this.props.muted, videoMuted: !this.props.inFocus, videoMutedbyUser: false, messages: this.props.messages, participants: participants, showInviteModal: false, showDrawer: false, keyboardHeight: 0, 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, chatView: !videoEnabled, audioView: !videoEnabled, isLandscape: this.props.isLandscape, selectedContacts: this.props.selectedContacts, activeDownloads: {}, offset : 0, topHeight : Dimensions.get('window').height - bottomHeight, bottomHeight : duration > 10 && this.props.conferenceSliderPosition ? this.props.conferenceSliderPosition : bottomHeight, // min height for bottom pane header, deviceHeight : Dimensions.get('window').height, isDividerClicked: false, pan : new Animated.ValueXY() }; this._panResponder = PanResponder.create({ onMoveShouldSetResponderCapture: () => true, onMoveShouldSetPanResponderCapture: () => true, // Initially, set the Y position offset when touch start onPanResponderGrant: (e, gestureState) => { this.setState({ offset: e.nativeEvent.pageY, isDividerClicked: true }) this.sliderTimeout = setTimeout(() => { this.setState({ isDividerClicked: false }) }, 2000); }, // When we drag the divider, set the bottomHeight (component state) again. onPanResponderMove: (e, gestureState) => { //let b = gestureState.moveY > (this.state.deviceHeight - 40) ? 40 : this.state.deviceHeight - gestureState.moveY - 40; const maxH = Dimensions.get('window').height - this.audioViewMinHeight - 110; let b = Math.floor(this.state.deviceHeight - gestureState.moveY); if (b > maxH) { b = maxH; } var d = this.state.bottomHeight - b; if (d < 0) { d = -d; } if (d >= 30) { this.setState({ bottomHeight : b, offset: e.nativeEvent.pageY, isDividerClicked: true }) if (this.sliderTimeout) { clearTimeout(this.sliderTimeout); this.sliderTimeout = null; } this.sliderTimeout = setTimeout(() => { console.log('Turn slider off'); this.setState({ isDividerClicked: false }) this.sliderTimeout = null; }, 2000); this.props.saveSliderFunc(b); } }, onPanResponderRelease: (e, gestureState) => { // Do something here for the touch end event this.setState({ offset: e.nativeEvent.pageY, }) } }); const friendlyName = this.state.remoteUri.split('@')[0]; //if (window.location.origin.startsWith('file://')) { this.conferenceUrl = `${config.publicUrl}/conference/${friendlyName}`; //} else { // this.conferenceUrl = `${window.location.origin}/conference/${friendlyName}`; //} const emailMessage = `You can join me in the conference using a Web browser at ${this.conferenceUrl} ` + '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.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(); // TODO preserve this list between route changes console.log('Initial call duration', duration); props.initialParticipants.forEach((uri) => { const existing_participants = participants.filter(p => p.identity._uri === uri); if (existing_participants.length === 0) { this.invitedParticipants.set(uri, {timestamp: Date.now(), status: duration < 10 ? 'Invited' : 'No answer'}) this.lookupContact(uri); } }); this.participantsTimer = setInterval(() => { this.updateParticipantsStatus(); }, this.sampleInterval * 1000); this.props.getMessages(this.state.remoteUri.split('@')[0]); setTimeout(() => { this.listSharedFiles(); }, 1000); } get chatViewHeight() { const wh = Dimensions.get('window').height; const kh = this.state.keyboardHeight; const sh = (Platform.OS === 'android') ? StatusBar.currentHeight : 0; //console.log('window height', Math.floor(wh)); //console.log('keyboa height', Math.floor(kh)); //console.log('status height', Math.floor(sh)); let ah = Platform.OS === 'android' ? wh - kh - sh - 30: wh - 50; //console.log('Available height', Math.floor(ah)); return ah; } messageExists(giftedChatMessage, sylkMessage) { if (sylkMessage._id === giftedChatMessage._id) { return true; } let gs_timestamp = giftedChatMessage.createdAt; let sylk_timestamp = sylkMessage.timestamp; gs_timestamp.setMilliseconds(0); sylk_timestamp.setMilliseconds(0); if (gs_timestamp.toString() === sylk_timestamp.toString() && giftedChatMessage.text === sylkMessage.content) { return true; } return false; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.hasOwnProperty('isDividerClicked')) { this.setState({isDividerClicked: nextProps.isDividerClicked}); } if (nextProps.hasOwnProperty('keyboardVisible')) { this.setState({keyboardVisible: nextProps.keyboardVisible}); } if (nextProps.call !== null && nextProps.call !== this.state.call) { this.setState({call: nextProps.call}); } 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}); } let renderMessages = []; if (nextProps.remoteUri in nextProps.messages) { nextProps.messages[nextProps.remoteUri].forEach((message) => { const existingMessages = this.state.renderMessages.filter(msg => msg._id === message._id); if (existingMessages.length > 0) { return; } renderMessages.push(message); }); if (nextProps.call) { this.setState({sharedFiles: nextProps.call.sharedFiles.slice()}); let giftedChatMessage; let existingMessages; let previousMessages; nextProps.call.messages.forEach((sylkMessage) => { if (sylkMessage.type === 'status') { return; } if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } existingMessages = renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } existingMessages = this.state.renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } let direction = sylkMessage.state === 'received' ? 'incoming': 'outgoing'; if (direction === 'incoming' && sylkMessage.sender.uri === this.props.account.id) { direction = 'outgoing'; } - giftedChatMessage = utils.sylkToRenderMessage(sylkMessage, null, direction); + giftedChatMessage = utils.sylk2GiftedChat(sylkMessage, null, direction); renderMessages.push(giftedChatMessage); this.saveConferenceMessage(this.props.remoteUri, giftedChatMessage); }); } } if (nextProps.bottomHeight) { this.setState({ topHeight : nextProps.keyboardVisible === false ? nextProps.topHeight : 0, // min height for top pane heade bottomHeight : nextProps.bottomHeight, // min height for bottom pane header, }); } this.setState({terminated: nextProps.terminated, remoteUri: nextProps.remoteUri, renderMessages: GiftedChat.append(this.state.renderMessages, renderMessages), isLandscape: nextProps.isLandscape, messages: nextProps.messages, offset: nextProps.offset, activeDownloads: nextProps.activeDownloads, accountId: !this.state.accountId && nextProps.call ? this.props.call.account.id : this.state.accountId, selectedContacts: nextProps.selectedContacts}); } getInfo() { let info; let bandwidthDownload = this.bandwidthDownload; let bandwidthUpload = this.bandwidthUpload; let unit = 'Kbit/s'; if (this.bandwidthDownload > 0 && this.bandwidthUpload > 0) { if (this.bandwidthDownload > 1100 || this.bandwidthUpload > 1100) { bandwidthDownload = Math.ceil(this.bandwidthDownload / 1000 * 100) / 100; bandwidthUpload = Math.ceil(this.bandwidthUpload / 1000 * 100) / 100; unit = 'Mbit/s'; } info = '⇣' + bandwidthDownload + ' ⇡' + bandwidthUpload + ' ' + unit; } return info; } saveConferenceMessage(uri, message) { this.props.saveConferenceMessage(uri, message); } updateConferenceMessage(uri, message) { this.props.updateConferenceMessage(uri, message); } onSendFromUser() { console.log('On send from user...'); } uploadBegin(response) { var jobId = response.jobId; console.log('UPLOAD HAS BEGUN! JobId: ' + jobId); }; uploadProgress(response) { var percentage = Math.floor((response.totalBytesSent/response.totalBytesExpectedToSend) * 100); console.log('UPLOAD IS ' + percentage + '% DONE!'); }; transferComplete(evt) { console.log("Upload has finished", evt); } transferFailed(evt) { console.log("An error occurred while transferring the file.", evt); } transferCanceled(evt) { console.log("The transfer has been canceled by the user."); } filePath(filename) { let dir = RNFS.DocumentDirectoryPath + '/conference/' + this.state.remoteUri + '/files'; let path; RNFS.mkdir(dir); path = dir + '/' + filename.toLowerCase(); return path; } tsize(fsize) { let size = fsize + + " B"; if (fsize > 1024 * 1024) { size = Math.ceil(fsize/1024/1024) + " MB"; } else if (fsize < 1024 * 1024) { size = Math.ceil(fsize/1024) + " KB"; } return size; } toggleDownload(metadata) { //console.log('toggleDownload', metadata); let renderMessages = this.state.renderMessages; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id === metadata.transfer_id) { //console.log('Found message', msg.metadata); if (msg.metadata.progress === null) { msg.metadata.progress = 0; msg.metadata.failed = false; //console.log('Start metadata', msg.metadata); this.downloadFile(metadata); } else { //console.log('Stop metadata', msg.metadata); this.stopDownloadFile(metadata); msg.metadata.progress = null; } this.updateConferenceMessage(this.props.remoteUri, msg); } }); } async _launchCamera() { let options = {saveToPhotos: true, mediaType: 'photo', maxWidth: 2000, cameraType: 'front' } await launchCamera(options, this.cameraCallback); } async _launchImageLibrary() { let options = {}; await launchImageLibrary(options, this.cameraCallback); } cameraCallback (result) { if (result.assets) { this.uploadFile(result.assets[0]); } } async _pickDocument() { try { const result = await DocumentPicker.pick({ type: [DocumentPicker.types.allFiles], copyTo: 'documentDirectory', mode: 'import', allowMultiSelection: false, }); const fileUri = result[0].fileCopyUri; if (!fileUri) { console.log('File URI is undefined or null'); return; } console.log('Send file', fileUri); this.uploadfile(fileUri); } catch (err) { if (DocumentPicker.isCancel(err)) { console.log('User cancelled file picker'); } else { console.log('DocumentPicker err => ', err); throw err; } } }; renderSend = (props) => { let chatRightActionsContainer = Platform.OS === 'ios' ? styles.chatRightActionsContaineriOS : styles.chatRightActionsContainer; return ( ); }; renderMessageImage =(props) => { return ( ) } renderMessageVideo(props){ const { currentMessage } = props; return ( ); }; renderCustomView(props) { const {currentMessage} = props; const { text: currText } = currentMessage; if (!currentMessage.metadata) { return null; } let status = ''; let label = 'Uploading...'; let showSwitch = currentMessage.download || (currentMessage.url && (currentMessage.metadata.progress || !currentMessage.metadata.progress !== 100) && !currentMessage.local_url && !utils.isImage(currentMessage.metadata.name)) ; let switchOn = (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) ? true : false; if (currentMessage.direction === 'incoming') { label = 'Downloading...'; if (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) { status = currentMessage.label + ' - ' + currentMessage.metadata.progress + '%'; } else { if (!utils.isImage(currentMessage.metadata.name)) { status = 'Swipe to download \n' + currentMessage.label; } else { status = currentMessage.label; } } } else { if (!currentMessage.local_url && currentMessage.metadata.progress === null) { switchOn = false; } if (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) { status = currentMessage.label + ' - ' + currentMessage.metadata.progress + '%'; } else { status = currentMessage.label; } } if (currentMessage.url && !currentMessage.local_url) { //console.log('--- Render message', currentMessage.metadata.name, currentMessage.metadata.progress); } if (!utils.isImage(currentMessage.metadata.name) && !currentMessage.local_url) { //console.log('Show switch', currentMessage._id, currentMessage.metadata.name, switchOn, currentMessage.metadata.progress); } //console.log('text =', currentMessage.text, 'label =', label, 'status =', status); let progress = 'Download'; if (currentMessage.metadata.progress !== null) { progress = currentMessage.metadata.progress + ' %'; } if (showSwitch) { return ( {progress} this.toggleDownload(currentMessage.metadata)}/> ); } else { return null; } }; failedFileUploadMessage(id) { let renderMessages = this.state.renderMessages; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id === id) { msg.sent = true; msg.received = false; msg.failed = true; msg.metadata.progress = null; msg.metadata.started = false; } newRenderMessages.push(msg); this.updateConferenceMessage(this.state.remoteUri, msg); }); } async uploadFile(fileObject) { console.log('Uploading file', fileObject); var id = md5.hex_md5(this.state.remoteUri + '_' + basename); let filepath = fileObject.uri ? fileObject.uri : fileObject; const basename = filepath.split('\\').pop().split('/').pop(); let stats_filename = filepath.startsWith('file://') ? filepath.substr(7, filepath.length - 1) : filepath; const { size } = await RNFetchBlob.fs.stat(stats_filename); let file_transfer = { 'path': filepath, 'filename': basename, 'filesize': fileObject.fileSize || size, 'sender': {'uri': this.state.accountId}, 'receiver': {'uri': this.state.remoteUri}, 'transfer_id': id, 'direction': 'outgoing' }; if (fileObject.filetype) { file_transfer.filetype = fileObject.filetype; } let text = utils.beautyFileNameForBubble(file_transfer); let msg = { _id: id, key: id, text: text, metadata: file_transfer, received: false, sent: false, pending: true, createdAt: new Date(), direction: 'outgoing', user: {} } if (utils.isImage(basename)) { msg.image = filepath; } else if (utils.isAudio(basename)) { msg.audio = filepath; } else if (utils.isVideo(basename)) { msg.video = filepath; } this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); file_transfer.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + this.props.call.id + '/' + basename; file_transfer.transfer_id = id; let localPath = this.filePath(basename); await RNFS.copyFile(file_transfer.path, localPath); //console.log('Copy file to', localPath); file_transfer.local_url = localPath; file_transfer.progress = 0; msg.metadata = file_transfer; RNFS.readFile(localPath, 'base64').then(res => { this.saveConferenceMessage(this.state.remoteUri, msg); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); var oReq = new XMLHttpRequest(); oReq.addEventListener("load", this.transferComplete); oReq.addEventListener("error", this.transferFailed); oReq.addEventListener("abort", this.transferCanceled); oReq.open('POST', file_transfer.url); const formData = new FormData(); formData.append(res); oReq.send(formData); if (oReq.upload) { oReq.upload.onprogress = ({ total, loaded }) => { const progress = Math.ceil(loaded / total * 100); this.updateFileMessage(id, progress); }; } }) .catch(err => { console.log('Failed to upload file', err.message, err.code); }); } updateFileMessage(id, progress, failed=false) { //make a change togglePlay(msgidx) { //console.log('Update file progress', id, progress); let renderMessages = this.state.renderMessages; let newRenderMessages = []; let nextState; renderMessages.forEach((msg) => { if (msg._id === id) { //console.log('Update file transfer for msg', msg); if (failed) { msg.failed = true; msg.sent = true; msg.pending = false; msg.received = false; msg.metadata.progress = null; this.postChatSystemMessage('Download failed', false); this.updateConferenceMessage(this.state.remoteUri, msg); } msg.metadata.progress = progress; if (progress !== null) { msg.failed = false; msg.received = null; } if (progress === 100 && (!msg.sent || !msg.received)) { msg.failed = false; msg.pending = false; msg.sent = msg.direction === 'outgoing' ? true : false; msg.received = true; msg.text = utils.beautyFileNameForBubble(msg.metadata); console.log(msg.metadata.filename, msg.direction === 'outgoing' ? 'Upload completed' : 'Download completed'); //console.log('Update metadata', msg.metadata); this.updateConferenceMessage(this.state.remoteUri, msg); } } newRenderMessages.push(msg); }); this.setState({renderMessages: GiftedChat.append(newRenderMessages, [])}); } purgeSharedFiles() { this.state.renderMessages.forEach((msg) => { if (msg.url) { if (!msg.image && !msg.local_url) { const parts = msg.url.split('/'); const filename = parts[parts.length - 1]; let existingFiles = this.state.sharedFiles.filter(file => md5.hex_md5(this.state.remoteUri + '_' + filename) === msg._id); if (existingFiles.length === 0) { this.props.deleteConferenceMessage(this.state.remoteUri, msg); } } } }); } async listSharedFiles() { console.log('--- List shared files'); let messages = this.state.renderMessages; let new_messages = []; let found = false; let exists = false; for (const file of this.state.sharedFiles) { if (file.session === this.props.call.id) { // skip my own files continue; } let metadata = {}; let text; let url; let msg; found = false; exists = false; metadata.transfer_id = md5.hex_md5(this.state.remoteUri + '_' + file.filename); for (const msg of messages) { if (msg._id === metadata.transfer_id) { found = true; metadata = msg.metadata; console.log('File transfer', metadata.filename, 'already exists'); msg.text = utils.beautyFileNameForBubble(metadata); exists = await RNFS.exists(metadata.local_url); if (exists) { console.log('Local file', metadata.filename, 'already exists'); metadata.received = true; if (utils.isImage(metadata.filename)) { msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } else if (utils.isAudio(metadata.filename)) { msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } else if (utils.isVideo(metadata.filename)) { msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } } else { metadata.received = false; msg.image = null; msg.audio = null; msg.video = null; } console.log('Updated message', msg); new_messages.push(msg); } } if (found) { this.setState({renderMessages: GiftedChat.append(new_messages, [])}); console.log('Update list and return'); return; } metadata.filesize = file.filesize; metadata.filename = file.filename; metadata.sender = {uri: file.uploader.uri}; metadata.receiver = {uri: this.state.remoteUri}; metadata.session = file.session; metadata.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + metadata.session + '/' + metadata.name; metadata.direction = metadata.sender.uri === this.props.account.id ? 'outgoing' : 'incoming'; metadata.local_url = this.filePath(metadata.filename); console.log('--- Shared file:', metadata); text = utils.beautyFileNameForBubble(metadata); msg = { _id: metadata.transfer_id, key: metadata.transfer_id, createdAt: new Date(), text: text, url: url, metadata: metadata, received: false, failed: false, sent: false, user: metadata.direction === 'incoming' ? {_id: metadata.sender.uri, name: metadata.sender.displayName || metadata.sender.uri} : {} }; exists = await RNFS.exists(metadata.local_url); if (exists) { console.log('Local file new', metadata.local_url, 'already exists'); metadata.received = true; if (utils.isImage(metadata.filename)) { msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } else if (utils.isAudio(metadata.filename)) { msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } else if (utils.isVideo(metadata.filename)) { msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } } else { metadata.progress = 0; if (isImage) { this.downloadFile(metadata); } } this.saveConferenceMessage(this.state.remoteUri, msg); console.log('Adding message for file transfer', msg); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); } setTimeout(() => { this.purgeSharedFiles(); }, 1000); } async stopDownloadFile(metadata) { let renderMessages = this.state.renderMessages; renderMessages.forEach((msg) => { if (msg._id === metadata.transfer_id) { msg.metadata.progress = null; this.updateConferenceMessage(this.state.remoteUri, msg); } }); if (metadata.transfer_id in this.downloadRequests) { console.log('Stop download', metadata.url); let task = this.downloadRequests[metadata.transfer_id]; task.stop(); delete this.downloadRequests[metadata.transfer_id]; } } async downloadFile(metadata) { //console.log('downloadFile', metadata); let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); /* TODO: server needs support for this resume if (metadata.transfer_id in this.downloadRequests) { let task = this.downloadRequests[metadata.transfer_id]; console.log('Resume download', metadata.url); task.resume(); return; } */ const existingTask = lostTasks.filter(task => task.id === metadata.transfer_id); if (existingTask.length === 1) { var task = existingTask[0]; console.log('Found existing download task', task); task.progress((percent) => { const progress = Math.ceil(percent * 100); this.updateFileMessage(metadata.transfer_id, progress); }).begin((expectedBytes) => { this.updateFileMessage(metadata.transfer_id, 0); }).done(() => { this.updateFileMessage(metadata.transfer_id, 100); }).error((error) => { this.updateFileMessage(metadata.transfer_id, 0, error); console.log(task.url, 'download error:', error); }); } else { console.log('Start new download:', metadata.url); this.updateFileMessage(metadata.transfer_id, 0); this.downloadRequests[metadata.transfer_id] = RNBackgroundDownloader.download({ id: metadata.transfer_id, url: metadata.url, destination: metadata.local_url }).begin((expectedBytes) => { this.updateFileMessage(metadata.transfer_id, 0); console.log(metadata.name, 'will download', expectedBytes, 'bytes'); }).progress((percent) => { const progress = Math.ceil(percent * 100); this.updateFileMessage(metadata.transfer_id, progress); }).done(() => { this.updateFileMessage(metadata.transfer_id, 100); delete this.downloadRequests[metadata.transfer_id]; }).error((error) => { console.log(metadata.name, 'download error:', error); this.updateFileMessage(metadata.transfer_id, 0, error); delete this.downloadRequests[metadata.transfer_id]; }); } } onLongMessagePress(context, currentMessage) { if (currentMessage && currentMessage.text) { let options = [] options.push('Copy'); if (currentMessage.local_url) { options.push('Open'); } options.push('Cancel'); //console.log('currentMessage', currentMessage); let l = options.length - 1; context.actionSheet().showActionSheetWithOptions({options, l}, (buttonIndex) => { let action = options[buttonIndex]; if (action === 'Copy') { Clipboard.setString(currentMessage.text); } else if (action === 'Open') { FileViewer.open(currentMessage.local_url, { showOpenWithDialog: true }) .then(() => { // success }) .catch(error => { // error }); } }); } }; removeInvitedParticipant(uri) { if (this.invitedParticipants.has(uri) > 0) { this.invitedParticipants.delete(uri); this.forceUpdate(); } } updateParticipantsStatus() { let participants_uris = []; this.state.participants.forEach((p) => { participants_uris.push(p.identity._uri); }); this.getConnectionStats(); 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); if (!p) { return; } interval = Math.floor((Date.now() - p.timestamp) / 1000); if (p.status == 'No answer' && interval >= 15) { //this.invitedParticipants.delete(_uri); //console.log('Update status', _uri, p.status); p.status = 'reinvite'; interval = 0; } if (p.status.indexOf('Invited') > -1 && interval > 5) { //console.log('Update status', _uri, p.status); p.status = 'Wait .'; } if (p.status.indexOf('.') > -1) { if (interval > 10) { //console.log('Update status', _uri, p.status); p.status = 'No answer'; this.postChatSystemMessage(_uri + ' did not answer', false); } else { //console.log('Update status', _uri, p.status); p.status = p.status + '.'; } } }); this.forceUpdate(); } postChatSystemMessage(text, save=true) { var now = new Date(); var hours = now.getHours(); var mins = now.getMinutes(); var secs = now.getSeconds(); var ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; mins = mins < 10 ? '0' + mins : mins; secs = secs < 10 ? '0' + secs : secs; text = text + ' at ' + hours + ":" + mins + ':' + secs + ' ' + ampm; var id = uuid.v4(); const giftedChatMessage = { _id: uuid.v4(), key: id, createdAt: now, text: text, system: true, }; this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); if (save) { this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); } } componentDidMount() { for (let p of this.state.participants) { p.on('stateChanged', this.onParticipantStateChanged); p.attach(); } this.keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', this._keyboardDidShow ); this.keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', this._keyboardDidHide ); 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.props.call.on('composingIndication', this.composingIndicationReceived); this.props.call.on('message', this.messageReceived); 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.state.remoteUri, p.identity._uri); this.lookupContact(p.identity._uri, p.identity._displayName); } }); // this.changeResolution(); } if (this.state.videoMuted) { this._muteVideo(); } //let msg = "Others can join the conference using a web browser at " + this.conferenceUrl; //this.postChatSystemMessage(msg, false); if (this.state.selectedContacts) { this.inviteParticipants(this.state.selectedContacts); } } componentWillUnmount() { clearTimeout(this.overlayTimer); clearTimeout(this.participantsTimer); this.uploads.forEach((upload) => { this.props.notificationCenter().removeNotification(upload[1]); upload[0].abort(); }) this.keyboardDidShowListener.remove(); this.keyboardDidHideListener.remove(); } _keyboardDidShow(e) { this.setState({keyboardVisible: true, keyboardHeight: e.endCoordinates.height}); } _keyboardDidHide() { this.setState({keyboardVisible: false, keyboardHeight: 0}); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } composingIndicationReceived(data) { if (this.typingTimer) { clearTimeout(this.typingTimer); } this.setState({isTyping: true}); this.typingTimer = setTimeout(() => { this.setState({isTyping: false}); this.typingTimer = null; }, 5000); } messageReceived(sylkMessage) { //console.log('Conference got message', sylkMessage); if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } const existingMessages = this.state.renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } if (sylkMessage.direction === 'incoming' && sylkMessage.sender.uri === this.state.accountId) { sylkMessage.direction = 'outgoing'; } - const giftedChatMessage = utils.sylkToRenderMessage(sylkMessage); + const giftedChatMessage = utils.sylk2GiftedChat(sylkMessage); if (sylkMessage.type === 'status') { return; } this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); } onSendMessage(messages) { if (!this.props.call) { return; } messages.forEach((message) => { this.props.sendConferenceMessage(message); }); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, messages)}); } lookupContact(uri, displayName) { let photo; let username = uri.split('@')[0]; if (this.props.myContacts.hasOwnProperty(uri) && this.props.myContacts[uri].name) { displayName = this.props.myContacts[uri].name; } else 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, 'uri', username); } else { var contact_obj = this.findObjectByKey(this.props.contacts, 'uri', 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) } getConnectionStats() { let audioPackets = 0; let videoPackets = 0; let delay = 0; let audioPacketsLost = 0; let videoPacketsLost = 0; let audioPacketLoss = 0; let videoPacketLoss = 0; let totalPackets = 0; let totalPacketsLost = 0; let totalPacketLoss = 0; let totalAudioBandwidth = 0; let totalVideoBandwidth = 0; let totalSpeed = 0; let bandwidthUpload = 0; let mediaType; if (this.state.participants.length === 0) { this.bandwidthDownload = 0; this.videoBandwidth.set('total', 0); this.audioBandwidth.set('total', 0); } let participants = this.state.participants.concat(this.props.call); participants.forEach((p) => { if (!p._pc) { return; } let identity; if (p.identity) { identity = p.identity.uri; } else { identity = 'myself'; } p._pc.getStats(null).then(stats => { audioPackets = 0; videoPackets = 0; audioPacketsLost = 0; videoPacketsLost = 0; audioPacketLoss = 0; videoPacketLoss = 0; stats.forEach(report => { if (report.type === "ssrc") { report.values.forEach(object => { if (object.mediaType) { mediaType = object.mediaType; } }); report.values.forEach(object => { if (object.bytesReceived && identity !== 'myself') { const bytesReceived = Math.floor(object.bytesReceived); if (mediaType === 'audio') { if (this.audioBytesReceived.has(p.id)) { const lastBytes = this.audioBytesReceived.get(p.id); const diff = bytesReceived - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); totalAudioBandwidth = totalAudioBandwidth + speed; totalSpeed = totalSpeed + speed; //console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity); this.audioBandwidth.set(p.id, speed); } this.audioBytesReceived.set(p.id, bytesReceived); } else if (mediaType === 'video') { if (this.videoBytesReceived.has(p.id)) { const lastBytes = this.videoBytesReceived.get(p.id); const diff = bytesReceived - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); totalVideoBandwidth = totalVideoBandwidth + speed; totalSpeed = totalSpeed + speed; //console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity); this.videoBandwidth.set(p.id, speed); } this.videoBytesReceived.set(p.id, bytesReceived); } } else if (object.bytesSent && identity === 'myself') { const bytesSent = Math.floor(object.bytesSent); if (mediaType === 'audio') { if (this.audioBytesReceived.has(p.id)) { const lastBytes = this.audioBytesReceived.get(p.id); const diff = bytesSent - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity); this.audioBandwidth.set(p.id, speed); } this.audioBytesReceived.set(p.id, bytesSent); } else if (mediaType === 'video') { if (this.videoBytesReceived.has(p.id)) { const lastBytes = this.videoBytesReceived.get(p.id); const diff = bytesSent - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity); this.videoBandwidth.set(p.id, speed); } this.videoBytesReceived.set(p.id, bytesSent); } } else if (object.totalAudioEnergy) { //console.log('Total audio energy', object.totalAudioEnergy, 'from', identity); } else if (object.audioOutputLevel) { //console.log('Output level', object.audioOutputLevel, 'from', identity); this.mediaLost.set(p.id, Math.floor(object.audioOutputLevel) < 5 ? true : false); } else if (object.audioInputLevel) { //console.log('Input level', object.audioInputLevel, 'from', identity); this.mediaLost.set(p.id, Math.floor(object.audioInputLevel) < 5 ? true : false); } else if (object.packetsLost) { totalPackets = totalPackets + Math.floor(object.packetsLost); totalPacketsLost = totalPacketsLost + Math.floor(object.packetsLost); if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsLost); audioPacketsLost = audioPacketsLost + Math.floor(object.packetsLost); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsLost); videoPacketsLost = videoPacketsLost + Math.floor(object.packetsLost); } if (object.packetsLost > 0) { //console.log(identity, mediaType, 'packetsLost', object.packetsLost); } } else if (object.packetsReceived && identity !== 'myself') { totalPackets = totalPackets + Math.floor(object.packetsReceived); if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsReceived); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsReceived); } //console.log(identity, mediaType, 'packetsReceived', object.packetsReceived); } else if (object.packetsSent && identity === 'myself') { totalPackets = totalPackets + Math.floor(object.packetsSent); if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsSent); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsSent); } //console.log(identity, mediaType, 'packetsSent', object.packetsSent); } else if (object.googCurrentDelayMs && identity !== 'myself') { delay = object.googCurrentDelayMs; //console.log('mediaType', mediaType, 'identity', identity, 'delay', delay); this.latency.set(p.id, Math.ceil(delay)); //console.log(object); } if (identity === 'myself') { //console.log(object); } }); if (videoPackets > 0) { videoPacketLoss = Math.floor(videoPacketsLost / videoPackets * 100); } else { videoPacketLoss = 100; } if (audioPackets > 0) { audioPacketLoss = Math.floor(audioPacketsLost / audioPackets * 100); } else { audioPacketLoss = 100; } if (totalPackets > 0) { totalPacketLoss = Math.floor(totalPacketsLost / totalPackets * 100); } else { totalPacketLoss = 100; } this.audioPacketLoss.set(p.id, audioPacketLoss); this.videoPacketLoss.set(p.id, videoPacketLoss); this.packetLoss.set(p.id, totalPacketLoss); }}); //console.log(identity, p.id, 'audio loss', audioPacketLoss, '%, video loss', videoPacketLoss, '%, total loss', totalPacketLoss, '%'); const bandwidthDownload = totalVideoBandwidth + totalAudioBandwidth; this.bandwidthDownload = bandwidthDownload; this.bandwidthUpload = bandwidthUpload; this.videoBandwidth.set('total', totalVideoBandwidth); this.audioBandwidth.set('total', totalAudioBandwidth); //console.log('audio bandwidth', totalAudioBandwidth); //console.log('video bandwidth', totalVideoBandwidth); //console.log('total bandwidth', this.bandwidthDownload); //console.log('this.latency', this.latency); }); }); }; onParticipantJoined(p) { console.log(p.identity.uri, 'joined the conference'); if (p.identity._uri.search('guest.') === -1) { if (p.identity._uri !== this.props.call.localIdentity._uri) { // used for history item this.props.saveParticipant(this.props.call.id, this.state.remoteUri, p.identity._uri); } const dn = p.identity._uri + ' joined'; this.postChatSystemMessage(dn, false); } else { this.postChatSystemMessage('An anonymous guest joined', false); } 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(); p.timestamp = Date.now(); this.setState({ participants: this.state.participants.concat([p]) }); // this.changeResolution(); this.armOverlayTimer(); } onParticipantLeft(p) { console.log(p.identity.uri, 'left the conference'); const participants = this.state.participants.slice(); this.audioBandwidth.delete(p.id); this.videoBandwidth.delete(p.id); this.latency.delete(p.id); this.audioBytesReceived.delete(p.id); this.videoBytesReceived.delete(p.id); this.audioPacketLoss.delete(p.id); this.videoPacketLoss.delete(p.id); this.packetLoss.delete(p.id); this.mediaLost.delete(p.id); const idx = participants.indexOf(p); if (idx !== -1) { participants.splice(idx, 1); this.setState({ participants: participants }); } p.detach(true); // this.changeResolution(); this.armOverlayTimer(); this.postChatSystemMessage(p.identity.uri + ' left', false); } 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}); this.listSharedFiles(); } 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.conferenceUrl); 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.toggleDrawer(); 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); } }); } toggleSpeakerSelection() { this.setState({showSpeakerSelection: !this.state.showSpeakerSelection}); } startSpeakerSelection(number) { this.selectSpeaker = number; this.toggleSpeakerSelection(); } preventOverlay(event) { // Stop the overlay when we are the thumbnail bar event.stopPropagation(); } muteAudio(event) { event.preventDefault(); if (this.state.audioMuted) { //this.postChatSystemMessage('Audio un-muted'); this.props.toggleMute(this.props.call.id, false); } else { //this.postChatSystemMessage('Audio muted'); this.props.toggleMute(this.props.call.id, true); } } toggleChat(event) { //event.preventDefault(); if (!this.state.videoEnabled) { if (this.state.chatView && !this.state.audioView) { this.setState({audioView: !this.state.audioView}); } } this.setState({chatView: !this.state.chatView}); } toggleAudioParticipants(event) { //event.preventDefault(); if (this.state.audioView && !this.state.chatView) { this.setState({chatView: !this.state.chatView}); } this.setState({audioView: !this.state.audioView}); } 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) { console.log('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) { console.log('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; } this.setState({callOverlayVisible: true}); if (this.state.participants.length > 0) { clearTimeout(this.overlayTimer); this.overlayTimer = setTimeout(() => { this.setState({callOverlayVisible: false}); }, 5000); } } 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=[]) { if (uris.length === 0) { return; } //console.log('inviteParticipants', uris); this.props.call.inviteParticipants(uris); uris.forEach((uri) => { uri = uri.replace(/ /g, ''); if (this.props.call.localIdentity._uri === uri) { return; } this.postChatSystemMessage(uri + ' was invited', false); this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'}) this.props.saveParticipant(this.props.call.id, this.state.remoteUri, uri); this.lookupContact(uri); }); this.props.finishInvite(); this.forceUpdate() } render() { if (this.props.call === null) { return (); } //console.log('---- Conference box', this.state.renderMessages.length); let watermark; let renderMessages = this.state.renderMessages; //renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); renderMessages = renderMessages.sort(function(a, b) { if (a.createdAt < b.createdAt) { return 1; //nameA comes first } if (a.createdAt > b.createdAt) { return -1; // nameB comes first } if (a.createdAt === b.createdAt) { if (a.msg_id < b.msg_id) { return 1; //nameA comes first } if (a.msg_id > b.msg_id) { return -1; // nameB comes first } } return 0; // names must be equal }); 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 buttons = {}; 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 conferenceContainer = this.state.isLandscape ? styles.conferenceContainerLandscape : styles.conferenceContainer; let chatContainer = this.state.isLandscape ? styles.chatContainerLandscape : styles.chatContainerPortrait; if (this.props.audioOnly) { chatContainer = this.state.isLandscape ? styles.chatContainerLandscapeAudio : styles.chatContainerPortraitAudio; } // populate speaker selection list only with participants that have video let speakerSelectionParticipants = []; this.state.participants.forEach((p) => { if (p.streams && p.streams.length > 0) { if (p.streams[0].getVideoTracks().length > 0) { let track = p.streams[0].getVideoTracks()[0]; speakerSelectionParticipants.push(p); } } }); //console.log('Number of possible speakers with video enabled', speakerSelectionParticipants.length); let myself = {id: this.props.call.id, publisherId: this.props.call.id, identity: this.props.call.localIdentity}; let unselectItem = {id: 'none', publisherId: null, identity: {uri: 'none', displayName: 'No speaker'}}; speakerSelectionParticipants.push(myself); speakerSelectionParticipants.push(unselectItem); //console.log('----speakerSelectionParticipants', speakerSelectionParticipants); const floatingButtons = []; if (this.state.videoEnabled && this.state.isLandscape) { floatingButtons.push( ); } if (!this.state.chatView && !this.state.showDrawer && speakerSelectionParticipants.length > 2 && this.state.videoEnabled) { floatingButtons.push( ); } if (this.state.videoEnabled) { floatingButtons.push( ); } if (!this.state.videoEnabled ) { floatingButtons.push( ); } if (!this.state.videoEnabled && !this.state.isLandscape) { /* floatingButtons.push( ); */ } if (this.state.videoEnabled && !this.state.chatView) { floatingButtons.push( ); } floatingButtons.push( ); if (this.state.videoEnabled && !this.state.chatView) { floatingButtons.push( ); } if (!this.state.reconnectingCall) { floatingButtons.push( ) } if (this.state.videoEnabled && !this.state.isLandscape) { floatingButtons.push( ); } /* floatingButtons.push( ); */ /* floatingButtons.push( ); */ if (this.props.isLandscape && !this.state.chatView && !this.props.audioOnly) { buttons.additional = floatingButtons; } else { buttons.additional = []; } /* buttons.additional.push( ); */ /* floatingButtons.push( ); */ const audioParticipants = []; let _contact; let _identity; let participants_uris = []; let sessionButtons = floatingButtons; let inviteParticipantsModal = ( {return p.identity.uri})} close={this.toggleInviteModal} room={this.state.remoteUri} defaultDomain = {this.props.defaultDomain} accountId = {this.props.call.localIdentity._uri} notificationCenter = {this.props.notificationCenter} lookupContacts = {this.props.lookupContacts} /> ); if (this.props.audioOnly) { sessionButtons = []; buttons.additional = []; 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); let status = ''; let duration = 0; if (p.timestamp) { duration = Math.floor(new Date() - p.timestamp) / 1000; if (duration > 3600) { status = moment.duration(new Date() - p.timestamp).format('hh:mm:ss', {trim: false}); } else { status = moment.duration(new Date() - p.timestamp).format('mm:ss', {trim: false}); } } audioParticipants.push( 10 ? this.packetLoss.get(p.id) : 0} timestamp={p.timestamp} isLocal={false} status={status} supportsVideo={this.state.call ? this.state.call.supportsVideo: false} /> ); }); const invitedParties = Array.from(this.invitedParticipants.keys()); let alreadyInvitedParticipants = [] 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 }; if (p.status != 'No answer') { alreadyInvitedParticipants.push(_uri) } //console.log('p.status', p.status); let extraButtons = []; let invite_uris = []; invite_uris.push(_uri); if (p.status === 'reinvite') { extraButtons.push( this.removeInvitedParticipant(_uri)} /> ); extraButtons.push( this.inviteParticipants(invite_uris)} /> ); } audioParticipants.push( ); }); const audioContainer = this.state.isLandscape ? styles.audioContainerLandscape : styles.audioContainerPortrait; audioParticipants.sort((a, b) => (a.timestamp < b.timestamp) ? 1 : -1) _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.splice(0, 0, ); if (this.state.isLandscape) { return ( {inviteParticipantsModal} {sessionButtons} {audioParticipants} { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} alwaysShowSend={true} scrollToBottom lockStyle={styles.lock} inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} + timeTextStyle={{ left: { color: 'white' }, right: { color: 'black' } }} infiniteScroll /> ); } else { return ( {inviteParticipantsModal} {!this.state.keyboardVisible && !this.props.isLandscape ? {sessionButtons} : null} {!this.state.keyboardVisible ? {audioParticipants} : null} {/* Divider */} {/* Bottom View */} {!this.state.isDividerClicked ? { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} alwaysShowSend={true} lockStyle={styles.lock} scrollToBottom inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} + timeTextStyle={{ left: { color: 'white' }, right: { color: 'black' } }} infiniteScroll /> : null} ); } } const participants = []; const drawerParticipants = []; if (this.state.participants.length > 0) { if (this.state.activeSpeakers.findIndex((element) => {return element.id === this.props.call.id}) === -1) { participants.push( ); } } drawerParticipants.push( ); let videos = []; let status = ''; if (this.state.participants.length === 0) { videos.push( ); } else { const activeSpeakers = this.state.activeSpeakers; const activeSpeakersCount = activeSpeakers.length; if (activeSpeakersCount > 0) { activeSpeakers.forEach((p) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms'; } if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } videos.push( ); }); this.state.participants.forEach((p) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms delay'; } if (this.state.activeSpeakers.indexOf(p) === -1) { participants.push( {}} pauseVideo={true} display={false} status={status} /> ); } drawerParticipants.push( ); }); } else { let vtrack; this.state.participants.forEach((p, idx) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms'; } if (p.streams && p.streams.length > 0) { if (p.streams[0].getVideoTracks().length > 0) { vtrack = p.streams[0].getVideoTracks()[0]; if (vtrack.muted) { //console.log('Skip muted video of', p.identity.uri); return; } } } // console.log('Added video of', p.identity.uri); videos.push( = 4) || (idx >= 2 && this.props.isTablet === false)} isLandscape={this.state.isLandscape} isTablet={this.props.isTablet} useTwoRows={this.state.participants.length > 2} status={status} /> ); if (idx >= 4 || idx >= 2 && this.props.isTablet === false) { participants.push( ); } drawerParticipants.push( ); }); } } const currentParticipants = this.state.participants.map((p) => {return p.identity.uri}) const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : []; if (this.state.callOverlayVisible) { buttons.bottom = floatingButtons; buttons.additional = []; } return ( {inviteParticipantsModal} {this.state.callOverlayVisible || this.state.chatView ? : null} {videos} {this.state.chatView ? { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} scrollToBottom inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} + timeTextStyle={{ left: { color: 'white' }, right: { color: 'black' } }} infiniteScroll /> : {participants} } {drawerParticipants} ); } } ConferenceBox.propTypes = { notificationCenter : PropTypes.func.isRequired, call : PropTypes.object, connection : PropTypes.object, hangup : PropTypes.func, saveParticipant : PropTypes.func, saveConferenceMessage: PropTypes.func, updateConferenceMessage : PropTypes.func, deleteConferenceMessage : PropTypes.func, messages : PropTypes.array, 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, initialParticipants : PropTypes.array, terminated : PropTypes.bool, myContacts : PropTypes.object, lookupContacts : PropTypes.func, goBackFunc : PropTypes.func, inviteToConferenceFunc: PropTypes.func, selectedContacts : PropTypes.array, callState : PropTypes.object, callContact : PropTypes.object, finishInvite : PropTypes.func, account : PropTypes.object, messages : PropTypes.object, getMessages : PropTypes.func, fileSharingUrl : PropTypes.string, sendConferenceMessage: PropTypes.func, conferenceSliderPosition: PropTypes.number, saveSliderFunc: PropTypes.func }; export default ConferenceBox;