diff --git a/app/assets/styles/blink/_ConferenceBox.scss b/app/assets/styles/blink/_ConferenceBox.scss index 089fe3c..155c5e5 100644 --- a/app/assets/styles/blink/_ConferenceBox.scss +++ b/app/assets/styles/blink/_ConferenceBox.scss @@ -1,199 +1,200 @@ .container { flex: 1; } .roundshape { height: 48; //any of height width: 48; //any of width justifyContent: center; borderRadius: 24px; } .whiteButton { background-color: white; } .disabledWhiteButton { background-color: rgba(#395936, .9); } .greenButton { background-color: rgba(#6DAA63, .9); } .disabledGreenButton { background-color: rgba(#395936, .9); } .hangupButton { background-color: rgba(#a94442, .8); } .buttonsContainer { flex-direction: row; justify-content: center; height: 55; border: 0px; } .hangupButtonAudioContainer { flex-direction: row; justify-content: center; margin-right: 15px; height: 55; border: 0px; } .hangupButtonVideoContainer { margin-top: 15px; } .hangupButtonVideoContainerLandscape { margin-right: 15px; } .conferenceContainerLandscape { flex-direction: row; align-content: flex-end; position: absolute; bottom: 0; left: 0; right: 0; top: 0; } .audioContainerLandscape { align-content: flex-start; margin-top: 55px; margin-left: 0px; margin-right: 5px; width: 50%; border: 0px; } .chatContainerLandscapeAudio { margin-top: 60px; margin-right: 0px; margin-left: 5px; border-color: grey; border-radius: 5px; width: 49%; border: 0px; } .chatContainerLandscape { margin-top: 65px; width: 400px; } .slider { - height: 20px; + height: 30px; justify-content: center; } .dotsContainer { height: 8px; flex-direction: row; justify-content: space-between; } .dots { margin-top: -19px; margin-right: 0px; } .dotsiOS { margin-top: -17px; margin-right: 0px; } .audioContainerPortrait { border: 0.5px; } .chatContainerPortraitAudio { +align-content: flex-end; } .chatContainerPortrait { flex: 1; border: 0px; border-radius: 5px; } .conferenceContainer { flex-direction: column; align-content: flex-start; position: absolute; bottom: 0; left: 0; right: 0; top: 0; } .videosContainer { flex: 1; flex-wrap: wrap; border-radius: 5px; } .carouselContainer { position: absolute; bottom: 10; left: 10; right: 10; } .landscapeVideosContainer { flex-direction: row; } .downloadContainer { flex-direction: row; justify-content: flex-end; align-items: center; } .switch { padding: 10px; } .uploadProgress { font-size: 14px; color: orange; } .button { background-color: white; margin: 8px; } .iosButton { background-color: white; margin: 8px; padding-top: 4px; } .androidButton { background-color: white; margin: 8px; } .hangupButton { background-color: rgba(#a94442, .8); } .wholePageVideo { width: 100%; height: 100%; } .landscapeDrawer { align-items: flex-end; justify-content: space-between; flex-direction: row; } .portraitDrawer { width: 300px; } diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js index 658f8d2..ec7013d 100644 --- a/app/components/ConferenceBox.js +++ b/app/components/ConferenceBox.js @@ -1,2744 +1,2764 @@ '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 } from 'react-native-gifted-chat' import xss from 'xss'; import CustomChatActions from './ChatActions'; 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 cloneDeep from 'lodash/cloneDeep'; 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); 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; + //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 >= 40) { + if (d >= 30) { this.setState({ bottomHeight : b, - offset: e.nativeEvent.pageY + 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, - isDividerClicked: false }) } }); 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); setTimeout(() => { this.listSharedFiles(); }, 1000); } get chatViewHeight() { - return Dimensions.get('window').height - this.state.keyboardHeight - 83; + 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); 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, - isDividerClicked: nextProps.isDividerClicked, activeDownloads: nextProps.activeDownloads, accountId: !this.state.accountId && nextProps.call ? this.props.call.account.id : this.state.accountId, selectedContacts: nextProps.selectedContacts}); } getInfo() { let info; if (this.bandwidthDownload > 0 && this.bandwidthUpload > 0) { info = '⇣' + this.bandwidthDownload + ' ⇡' + this.bandwidthUpload; } else if (this.bandwidthDownload > 0) { info = '⇣' + this.bandwidthDownload ; } else if (this.bandwidthUpload > 0) { info = '⇡' + this.bandwidthUpload; } if (info) { return info + ' Mbit/s'; } 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.msg_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); } }); } renderCustomActions = props => ( ) 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; } }; renderMessageBubble (props) { let rightColor = '#0084ff'; let leftColor = '#f0f0f0'; if (props.currentMessage.failed) { rightColor = 'red'; leftColor = 'red'; } else { if (props.currentMessage.pinned) { rightColor = '#2ecc71'; leftColor = '#2ecc71'; } } return ( ) } 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(file) { console.log('Uploading file', file); let metadata = { name: file.name, type: file.type, size: file.size, uri: file.uri }; metadata.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + this.props.call.id + '/' + file.name;; metadata.msg_id = md5.hex_md5(this.state.remoteUri + '_' + file.name); metadata.local_url = this.filePath(metadata.name); metadata.progress = 0; if (metadata.size > 1024 * 1024 * 40) { this.postChatSystemMessage(metadata.name + 'is too big', false); return; } RNFS.readFile(metadata.uri, 'base64').then(res => { let image; let isImage = utils.isImage(metadata.name); let label = metadata.name.toLowerCase(); if (isImage) { image = metadata.uri; RNFS.copyFile(metadata.uri, metadata.local_url).then((success) => { console.log('Copy file to', metadata.local_url); image = 'file://' + metadata.local_url; }).catch((err) => { console.log('Error writing to file', metadata.local_url, err.message); }); label = this.tsize(metadata.size); } else { label = label + " (" + this.tsize(metadata.size) + ")"; } const giftedChatMessage = { _id: metadata.msg_id, key: metadata.msg_id, createdAt: new Date(), text: label, metadata: metadata, received: false, sent: false, pending: true, label: label, direction: 'outgoing', image: image, user: {} }; //console.log('metadata', metadata); this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); var oReq = new XMLHttpRequest(); oReq.addEventListener("load", this.transferComplete); oReq.addEventListener("error", this.transferFailed); oReq.addEventListener("abort", this.transferCanceled); oReq.open('POST', metadata.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(metadata.msg_id, progress); }; } }) .catch(err => { console.log(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', id, msg.metadata.name, progress, failed); if (failed) { msg.failed = true; msg.sent = true; msg.pending = false; msg.received = false; msg.metadata.progress = null; console.log('Message failed'); this.postChatSystemMessage('Download failed', false); this.updateConferenceMessage(this.state.remoteUri, msg); return; } msg.metadata.progress = progress; if (progress !== null) { msg.failed = false; msg.received = null; } if (progress === 100 && (!msg.sent || !msg.received)) { msg.local_url = msg.metadata.local_url; msg.url = msg.metadata.url; msg.metadata.progress = null; msg.failed = false; msg.pending = false; msg.sent = msg.direction === 'outgoing' ? true : false; msg.received = true; if (msg.image || utils.isImage(msg.metadata.name)) { msg.text = 'Image of ' + this.tsize(msg.metadata.size); msg.image = 'file://' + msg.metadata.local_url; } else { msg.text = msg.metadata.name + " (" + this.tsize(msg.metadata.size) + ")"; } console.log(msg.metadata.name, msg.direction === 'outgoing' ? 'Upload completed' : 'Download completed'); //console.log('Update metadata', msg.metadata); this.updateConferenceMessage(this.state.remoteUri, msg); } newRenderMessages.push(cloneDeep(msg)); } else { newRenderMessages.push(msg); } }); this.setState({ renderMessages: 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() { let url; let i = 0; let image; - console.log('--- List shared files'); + //console.log('--- List shared files'); this.state.sharedFiles.forEach((file)=>{ if (file.session === this.props.call.id) { // skip my own files return; } let metadata = {}; metadata.size = file.filesize; metadata.name = file.filename; metadata.uploader = file.uploader; metadata.session = file.session; console.log('--- Shared file:', metadata.uploader.uri, metadata.name); image = null; url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + metadata.session + '/' + metadata.name; metadata.url = url; metadata.msg_id = md5.hex_md5(this.state.remoteUri + '_' + metadata.name); let label = metadata.name.toLowerCase(); label = label + " (" + this.tsize(metadata.size) + ")"; const existingMessages = this.state.renderMessages.filter(msg => msg._id === metadata.msg_id); if (existingMessages.length > 0) { //console.log('Message already exists', metadata.msg_id ); return; } const direction = metadata.uploader.uri === this.props.account.id ? 'outgoing' : 'incoming'; let text = metadata.name.toLowerCase(); let isImage = utils.isImage(metadata.name); if (isImage) { text = 'Image of '; } text = text + " (" + this.tsize(metadata.size) + ")"; metadata.local_url = this.filePath(metadata.name); RNFS.exists(metadata.local_url).then(res => { if (res) { //console.log('File', file.name, 'already exists'); if (isImage) { image = 'file://' + metadata.local_url; } const giftedChatMessage = { _id: metadata.msg_id, key: metadata.msg_id, createdAt: new Date(), text: text, url: url, local_url: metadata.local_url, metadata: metadata, image: image, received: false, failed: false, sent: false, user: direction === 'incoming' ? {_id: metadata.uploader.uri, name: metadata.uploader.displayName || metadata.uploader.uri} : {} }; this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); } else { metadata.progress = isImage ? 0 : null; const giftedChatMessage = { _id: metadata.msg_id, key: metadata.msg_id, createdAt: new Date(), text: text, url: url, metadata: metadata, label: label, failed: false, received: false, sent: false, direction: direction, user: direction === 'incoming' ? {_id: metadata.uploader.uri, name: metadata.uploader.displayName || metadata.uploader.uri} : {} }; this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); if (isImage) { this.downloadFile(metadata); } } }); i = i + 1; }); setTimeout(() => { this.purgeSharedFiles(); }, 1000); } async stopDownloadFile(metadata) { let renderMessages = this.state.renderMessages; renderMessages.forEach((msg) => { if (msg._id === metadata.msg_id) { msg.metadata.progress = null; this.updateConferenceMessage(this.state.remoteUri, msg); } }); if (metadata.msg_id in this.downloadRequests) { console.log('Stop download', metadata.url); let task = this.downloadRequests[metadata.msg_id]; task.stop(); delete this.downloadRequests[metadata.msg_id]; } } async downloadFile(metadata) { let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); /* TODO: server needs support for this resume if (metadata.msg_id in this.downloadRequests) { let task = this.downloadRequests[metadata.msg_id]; console.log('Resume download', metadata.url); task.resume(); return; } */ const existingTask = lostTasks.filter(task => task.id === metadata.msg_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.msg_id, progress); }).begin((expectedBytes) => { this.updateFileMessage(metadata.msg_id, 0); }).done(() => { this.updateFileMessage(metadata.msg_id, 100); }).error((error) => { this.updateFileMessage(metadata.msg_id, 0, error); console.log(task.url, 'download error:', error); }); } else { console.log('Start new download:', metadata.url); this.updateFileMessage(metadata.msg_id, 0); this.downloadRequests[metadata.msg_id] = RNBackgroundDownloader.download({ id: metadata.msg_id, url: metadata.url, destination: metadata.local_url }).begin((expectedBytes) => { this.updateFileMessage(metadata.msg_id, 0); console.log(metadata.name, 'will download', expectedBytes, 'bytes'); }).progress((percent) => { const progress = Math.ceil(percent * 100); this.updateFileMessage(metadata.msg_id, progress); }).done(() => { this.updateFileMessage(metadata.msg_id, 100); delete this.downloadRequests[metadata.msg_id]; }).error((error) => { console.log(metadata.name, 'download error:', error); this.updateFileMessage(metadata.msg_id, 0, error); delete this.downloadRequests[metadata.msg_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); 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 = Math.ceil(bandwidthDownload / 1000 * 100) / 100; this.bandwidthUpload = Math.ceil(bandwidthUpload / 1000 * 100) / 100; 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: 'yellow' } }} infiniteScroll /> + ); } else { - return ( - {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} - /> + {inviteParticipantsModal} {!this.state.keyboardVisible && !this.props.isLandscape ? {sessionButtons} : null} + {!this.state.keyboardVisible ? - - - + {audioParticipants} : null} {/* Divider */} - {!this.state.keyboardVisible ? - - - + + - - - : null} - {/* Bottom View */} - + {/* 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: 'yellow' } }} 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)); }} alwaysShowSend={true} scrollToBottom inverted={true} timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} 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; diff --git a/app/components/ConferenceHeader.js b/app/components/ConferenceHeader.js index ad5c222..0e782f4 100644 --- a/app/components/ConferenceHeader.js +++ b/app/components/ConferenceHeader.js @@ -1,293 +1,295 @@ import React, { useState, useEffect, useRef, Fragment, Component } from 'react'; import { View } from 'react-native'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import { Text, Appbar, Menu } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import styles from '../assets/styles/blink/_ConferenceHeader.scss'; class ConferenceHeader extends React.Component { constructor(props) { super(props); autoBind(this); this.state = { call: this.props.call, displayName: this.props.callContact ? this.props.callContact.name : this.props.remoteUri, callState: this.props.call ? this.props.call.state : null, participants: this.props.participants, startTime: this.props.callState ? this.props.callState.startTime : null, reconnectingCall: this.props.reconnectingCall, info: this.props.info, remoteUri: this.props.remoteUri, menuVisible: true, chatView: this.props.chatView, audioView: this.props.audioView } this.duration = null; this.timer = null; this._isMounted = false; this.menuRef = React.createRef(); } componentDidMount() { this._isMounted = true; if (!this.state.call) { return; } if (this.state.call.state === 'established') { this.startTimer(); } this.state.call.on('stateChanged', this.callStateChanged); this.setState({callState: this.state.call.state}); } startTimer() { if (this.timer !== null) { // already armed return; } // TODO: consider using window.requestAnimationFrame const startTime = this.state.startTime || new Date(); this.timer = setInterval(() => { const duration = moment.duration(new Date() - startTime); if (this.duration > 3600) { this.duration = duration.format('hh:mm:ss', {trim: false}); } else { this.duration = duration.format('mm:ss', {trim: false}); } if (this.props.show) { this.forceUpdate(); } }, 1000); } componentWillUnmount() { this._isMounted = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (!this._isMounted) { return; } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.call !== null && nextProps.call !== this.state.call) { nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call}); } this.setState({info: nextProps.info, remoteUri: nextProps.remoteUri, displayName: nextProps.callContact ? nextProps.callContact.name : nextProps.remoteUri, startTime: nextProps.callState ? nextProps.callState.startTime : null, chatView: nextProps.chatView, audioView: nextProps.audioView, participants: nextProps.participants}); } callStateChanged(oldState, newState, data) { if (newState === 'established' && this._isMounted && !this.props.terminated) { this.startTimer(); } if (newState === 'terminated') { if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); this.duration = null; this.timer = null; } if (!this._isMounted) { return; } this.setState({callState: newState}); } goBack() { this.props.goBackFunc(); } hangUp() { console.log('Hangup'); return; this.props.hangUpFunc(); } handleMenu(event) { - if (!this.state.menuVisible) { - return; - } + //console.log('handleMenu', event); switch (event) { case 'back': this.goBack(); break; case 'invite': this.props.inviteToConferenceFunc(); break; case 'hangup': this.hangUp(); break; case 'chat': this.props.toggleChatFunc(); break; case 'participants': this.props.toggleAudioParticipantsFunc(); break; case 'share': this.props.toggleInviteModal(); break; default: break; } this.setState({menuVisible: false}); } render() { //console.log('render conf header lanscape =', this.props.isLandscape); let videoHeader; let callButtons; if (this.props.terminated) { clearTimeout(this.timer); this.duration = null; this.timer = null; } const room = this.state.remoteUri.split('@')[0]; let displayName = (this.state.displayName && this.state.displayName !== this.state.remoteUri) ? this.state.displayName : room; - let callDetail; + let callDetail = ''; if (this.state.reconnectingCall) { callDetail = 'Reconnecting call...'; } else if (this.state.terminated) { callDetail = 'Conference ended'; } else if (this.duration) { callDetail = (this.props.isTablet ? 'Duration: ' : '') + this.duration; if (this.state.participants > 0) { var participants = this.state.participants + 1; callDetail = callDetail + ' - ' + participants + ' participant' + (participants > 1 ? 's' : ''); } else { callDetail = callDetail + ' and nobody joined yet'; } } if (this.state.info) { - callDetail = callDetail + ' - ' + this.state.info; + if (callDetail) { + callDetail = callDetail + ' - ' + this.state.info; + } else { + callDetail = this.state.info; + } } let chatTitle = this.state.chatView ? 'Hide chat' : 'Show chat'; let participantsTitle = this.state.audioView ? 'Hide participants' : 'Show participants'; let buttonsContainerClass = this.props.isLandscape && !this.state.chatView ? styles.buttonsContainerLandscape : styles.buttonsContainer; return ( {this.goBack()}} /> {this.props.buttons.additional} this.handleMenu('invite')} icon="account-plus"/> this.handleMenu('share')} icon="share-variant"/> {this.props.buttons.bottom} ); } } /* This menu somehow causes the action button and menu itself to require double tap to be activated! this.setState({menuVisible: !this.state.menuVisible})} anchor={ this.setState({menuVisible: !this.state.menuVisible})} /> } > this.handleMenu('invite')} icon="account-plus" title="Invite participants..." /> this.handleMenu('share')} icon="share-variant" title="Share web link..." /> {!this.props.isLandscape ? this.handleMenu('chat')} icon="chat" title={chatTitle} /> : null} { this.props.audioOnly && !this.props.isLandscape? this.handleMenu('participants')} icon="account-multiple" title={participantsTitle} /> : null} this.handleMenu('hangup')} icon="phone-hangup" title="Hangup"/> */ ConferenceHeader.propTypes = { show: PropTypes.bool.isRequired, remoteUri: PropTypes.string.isRequired, call: PropTypes.object, isTablet: PropTypes.bool, isLandscape: PropTypes.bool, participants: PropTypes.number, buttons: PropTypes.object.isRequired, reconnectingCall: PropTypes.bool, audioOnly: PropTypes.bool, terminated: PropTypes.bool, info: PropTypes.string, callContact: PropTypes.object, toggleChatFunc: PropTypes.func, toggleAudioParticipantsFunc: PropTypes.func, goBackFunc: PropTypes.func, hangUpFunc: PropTypes.func, toggleInviteModal: PropTypes.func, inviteToConferenceFunc: PropTypes.func, audioView: PropTypes.bool, chatView: PropTypes.bool, callState: PropTypes.object }; export default ConferenceHeader; diff --git a/app/components/ReadyBox.js b/app/components/ReadyBox.js index 4141359..b2980b5 100644 --- a/app/components/ReadyBox.js +++ b/app/components/ReadyBox.js @@ -1,868 +1,868 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import autoBind from 'auto-bind'; import { FlatList, View, Platform, TouchableHighlight} from 'react-native'; import { IconButton, Title, Button, Colors } from 'react-native-paper'; import ConferenceModal from './ConferenceModal'; import ContactsListBox from './ContactsListBox'; import FooterBox from './FooterBox'; import URIInput from './URIInput'; import config from '../config'; import utils from '../utils'; import styles from '../assets/styles/blink/_ReadyBox.scss'; import {Keyboard} from 'react-native'; class ReadyBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { targetUri: '', contacts: this.props.contacts, selectedContact: this.props.selectedContact, showConferenceModal: this.props.showConferenceModal, sticky: false, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, historyCategoryFilter: null, historyPeriodFilter: null, missedCalls: this.props.missedCalls, isLandscape: this.props.isLandscape, participants: null, myInvitedParties: this.props.myInvitedParties, messages: this.props.messages, myDisplayName: this.props.myDisplayName, chat: (this.props.selectedContact !== null) && (this.props.call !== null), call: this.props.call, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectedContacts: this.props.selectedContacts, pinned: this.props.pinned, messageZoomFactor: this.props.messageZoomFactor, isTyping: this.props.isTyping, navigationItems: this.props.navigationItems, fontScale: this.props.fontScale, historyFilter: this.props.historyFilter, - isTable: this.props.isTablet, + isTablet: this.props.isTablet, myContacts: this.props.myContacts }; this.ended = false; } UNSAFE_componentWillReceiveProps(nextProps) { if (this.ended) { return; } if (this.state.selectedContact && !nextProps.selectedContact) { this.setState({targetUri: '', chat: false}); } if (!this.state.inviteContacts && nextProps.inviteContacts) { this.handleTargetChange(''); this.setState({chat: false}); } if (this.state.selectedContact !== nextProps.selectedContact && nextProps.selectedContact) { this.setState({chat: !this.chatDisabledForUri(nextProps.selectedContact.uri)}); } if (!nextProps.historyFilter && this.state.historyFilter) { this.filterHistory(null); } if (nextProps.missedCalls.length === 0 && this.state.historyCategoryFilter === 'missed') { this.setState({'historyCategoryFilter': null}); } if (nextProps.blockedUris.length === 0 && this.state.historyCategoryFilter === 'blocked') { this.setState({'historyCategoryFilter': null}); } if (nextProps.favoriteUris.length === 0 && this.state.historyCategoryFilter === 'favorite') { this.setState({'historyCategoryFilter': null}); } if (Object.keys(this.state.myContacts).length === 0 && nextProps.myContacts && Object.keys(nextProps.myContacts).length > 0) { this.bounceNavigation(); } this.setState({myInvitedParties: nextProps.myInvitedParties, myContacts: nextProps.myContacts, messages: nextProps.messages, historyFilter: nextProps.historyFilter, myDisplayName: nextProps.myDisplayName, call: nextProps.call, showConferenceModal: nextProps.showConferenceModal, isTyping: nextProps.isTyping, navigationItems: nextProps.navigationItems, messageZoomFactor: nextProps.messageZoomFactor, contacts: nextProps.contacts, inviteContacts: nextProps.inviteContacts, shareToContacts: nextProps.shareToContacts, selectedContacts: nextProps.selectedContacts, selectedContact: nextProps.selectedContact, pinned: nextProps.pinned, favoriteUris: nextProps.favoriteUris, blockedUris: nextProps.blockedUris, missedCalls: nextProps.missedCalls, fontScale: nextProps.fontScale, isTablet: nextProps.isTablet, isLandscape: nextProps.isLandscape}); } getTargetUri(uri) { return utils.normalizeUri(uri, this.props.defaultDomain); } async componentDidMount() { this.ended = false; } componentWillUnmount() { this.ended = true; } filterHistory(filter) { if (this.ended) { return; } this.props.filterHistoryFunc(filter); if (!filter) { this.setState({'historyPeriodFilter': null, historyCategoryFilter: null}); } else if (filter === 'today' || filter === 'yesterday') { filter = this.state.historyPeriodFilter === filter ? null : filter; this.setState({'historyPeriodFilter': filter}); } else { this.setState({'historyCategoryFilter': filter}); } this.handleTargetChange(''); } chatDisabledForUri(uri) { if (uri.indexOf('@videoconference') > -1) { return true; } if (uri.indexOf('@guest') > -1) { return true; } if (uri.indexOf('3333@') > -1) { return true; } if (uri.indexOf('4444@') > -1) { return true; } return false; } get showSearchBar() { if (this.state.selectedContact && !this.state.isTablet) { return false; } if (this.state.isTablet || (!this.state.isLandscape && this.state.selectedContact)) { return true; } if (this.state.call && this.state.call.state !== 'incoming' && !this.state.inviteContacts) { return false; } return true; } get showButtonsBar() { if (this.state.historyCategoryFilter === 'blocked') { return false; } if (this.state.isTablet) { return true; } if (this.state.call) { return true; } if (!this.state.targetUri) { return true; } if (this.state.selectedContact) { if (this.state.isLandscape && !this.state.isTablet) { return false; } return true; } return true; } handleTargetChange(new_uri, contact) { //console.log('---handleTargetChange new_uri =', new_uri); //console.log('handleTargetChange contact =', contact); if ((this.state.inviteContacts || this.state.shareToContacts) && contact) { const uri = contact.uri; this.props.updateSelection(uri); return; } if (this.state.selectedContact === contact) { if (this.state.chat) { this.setState({chat: false}); } return; } else { this.setState({chat: false}); } let new_value = new_uri; if (contact) { if (this.state.targetUri === contact.uri) { new_value = ''; } } else { contact = null; } if (this.state.targetUri === new_uri) { new_value = ''; } if (new_value === '') { contact = null; } if (new_value.indexOf(' ') === -1) { new_value = new_value.trim().toLowerCase(); } //new_value = new_value.replace(' ',''); //console.log('--- Select new contact', contact? contact.uri : null); //console.log('--- Select new targetUri', new_value); this.props.selectContact(contact); this.setState({targetUri: new_value}); } handleTargetSelect() { if (this.props.connection === null) { this.props._notificationCenter.postSystemNotification("Server unreachable"); return; } let uri = this.state.targetUri.toLowerCase(); if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { let participants; if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(uri)) { participants = this.state.myInvitedParties[uri]; } this.props.startConference(uri, {audio: true, video: true, participants: this.state.participants}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: true}); } } shareContent() { this.props.shareContent(); } showConferenceModal(event) { event.preventDefault(); this.props.showConferenceModalFunc(); } handleChat(event) { event.preventDefault(); let targetUri; if (!this.state.chat && !this.state.selectedContact) { targetUri = this.getTargetUri(this.state.targetUri); this.setState({targetUri: targetUri}); } let uri = this.state.targetUri.trim().toLowerCase(); if (!this.state.chat && !this.selectedContact && uri) { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.props.defaultDomain; } let contact = this.props.newContactFunc(uri, null, {src: 'new chat'}); console.log('Create synthetic contact', contact); this.props.selectContact(contact); this.setState({targetUri: uri, chat: true}); Keyboard.dismiss(); } this.setState({chat: !this.state.chat}); } handleAudioCall(event) { event.preventDefault(); Keyboard.dismiss(); let uri = this.state.targetUri.trim().toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(uri, {audio: true, video: false}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: false}); } } handleVideoCall(event) { event.preventDefault(); Keyboard.dismiss(); let uri = this.state.targetUri.toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(uri, {audio: true, video: true}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: true}); } } handleConferenceCall(targetUri, options={audio: true, video: true, participants: []}) { Keyboard.dismiss(); this.props.startConference(targetUri, {audio: options.audio, video: options.video, participants: options.participants}); this.props.hideConferenceModalFunc(); } get chatButtonDisabled() { let uri = this.state.targetUri.trim(); if (this.state.selectedContact) { return true; } if (this.state.shareToContacts) { return true; } if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } if (uri.indexOf('@') > -1) { let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; let validEmail = email_reg.test(uri); if (!validEmail) { return true; } } if (this.chatDisabledForUri(uri)) { return true; } return false; } get callButtonDisabled() { let uri = this.state.targetUri.trim(); if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } if (this.state.shareToContacts) { return true; } if (uri.indexOf('@') > -1) { let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; let validEmail = email_reg.test(uri); if (!validEmail) { return true; } } return false; } get videoButtonDisabled() { let uri = this.state.targetUri.trim(); if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } if (uri.indexOf('4444@') > -1) { return true; } if (this.state.shareToContacts) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } return this.callButtonDisabled; } get conferenceButtonDisabled() { let uri = this.state.targetUri.trim(); if (uri.indexOf(' ') > -1) { return true; } if (this.state.shareToContacts) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } if (uri.indexOf('@') > -1 && uri.indexOf(config.defaultConferenceDomain) === -1) { return true; } var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; if (event === 'call') { return true; } } return false; } renderNavigationItem(object) { if (!object.item.enabled) { return (null); } let title = object.item.title; let key = object.item.key; let buttonStyle = object.item.selected ? styles.navigationButtonSelected : styles.navigationButton; return (); } bounceNavigation() { setTimeout(() => { if (this.navigationRef && !this.state.selectedContact) { this.navigationRef.scrollToIndex({animated: true, index: Math.floor(this.navigationItems.length / 2)}); } }, 3000); setTimeout(() => { if (this.navigationRef && !this.state.selectedContact) { this.navigationRef.scrollToIndex({animated: true, index: this.navigationItems.length-1}); } }, 4500); setTimeout(() => { if (this.navigationRef && !this.state.selectedContact) { this.navigationRef.scrollToIndex({animated: true, index: 0}); } }, 6000); } get navigationItems() { let conferenceEnabled = Object.keys(this.state.myInvitedParties).length > 0 || this.state.navigationItems['conference']; if (this.state.inviteContacts) { conferenceEnabled = false; } return [ {key: null, title: 'All', enabled: true, selected: false}, {key: 'history', title: 'Calls', enabled: true, selected: this.state.historyCategoryFilter === 'history'}, {key: 'chat', title: 'Chat', enabled: true, selected: this.state.historyCategoryFilter === 'chat'}, {key: 'today', title: 'Today', enabled: this.state.navigationItems['today'], selected: this.state.historyPeriodFilter === 'today'}, {key: 'yesterday', title: 'Yesterday', enabled: this.state.navigationItems['yesterday'], selected: this.state.historyPeriodFilter === 'yesterday'}, {key: 'missed', title: 'Missed', enabled: this.state.missedCalls.length > 0, selected: this.state.historyCategoryFilter === 'missed'}, {key: 'favorite', title: 'Favorites', enabled: this.state.favoriteUris.length > 0, selected: this.state.historyCategoryFilter === 'favorite'}, {key: 'blocked', title: 'Blocked', enabled: this.state.blockedUris.length > 0, selected: this.state.historyCategoryFilter === 'blocked'}, {key: 'conference', title: 'Conference', enabled: conferenceEnabled, selected: this.state.historyCategoryFilter === 'conference'}, {key: 'test', title: 'Test', enabled: !this.state.shareToContacts && !this.state.inviteContacts, selected: this.state.historyCategoryFilter === 'test'}, ]; } render() { let uriClass = styles.portraitUriInputBox; let uriGroupClass = styles.portraitUriButtonGroup; let titleClass = styles.portraitTitle; let uri = this.state.targetUri.toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } /* console.log('Render -----'); if (this.state.selectedContact) { console.log('Render selectedContact', this.state.selectedContact.name); } if (this.state.callContact) { console.log('Render callContact', this.state.callContact.uri); } if (this.state.targetUri) { console.log('Render targetUri', this.state.targetUri); } - console.log('Render chat', this.state.chat); */ + //console.log('RB', this.state.isTablet); if (this.state.isTablet) { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTabletTitle : styles.portraitTabletTitle; } else { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTitle : styles.portraitTitle; } if (this.state.isTablet) { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriButtonGroup : styles.portraitTabletUriButtonGroup; } else { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeUriButtonGroup : styles.portraitUriButtonGroup; } if (this.state.isTablet) { uriClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriInputBox : styles.portraitTabletUriInputBox; } else { uriClass = this.props.orientation === 'landscape' ? styles.landscapeUriInputBox : styles.portraitUriInputBox; } const historyContainer = this.props.orientation === 'landscape' ? styles.historyLandscapeContainer : styles.historyPortraitContainer; const buttonGroupClass = this.props.orientation === 'landscape' ? styles.landscapeButtonGroup : styles.buttonGroup; const borderClass = this.state.chat ? null : styles.historyBorder; let backButtonTitle = 'Back to call'; const showBackToCallButton = this.state.call && this.state.call.state !== 'incoming' && this.state.call.state !== 'terminated' ? true : false ; if (showBackToCallButton) { if (this.state.call.hasOwnProperty('_participants')) { backButtonTitle = this.state.selectedContacts.length > 0 ? 'Invite people' : 'Back to conference'; } else { backButtonTitle = this.state.selectedContacts.length > 0 ? 'Invite people' : 'Back to call'; } } let greenButtonClass = Platform.OS === 'ios' ? styles.greenButtoniOS : styles.greenButton; let blueButtonClass = Platform.OS === 'ios' ? styles.blueButtoniOS : styles.blueButton; let disabledGreenButtonClass = Platform.OS === 'ios' ? styles.disabledGreenButtoniOS : styles.disabledGreenButton; let disabledBlueButtonClass = Platform.OS === 'ios' ? styles.disabledBlueButtoniOS : styles.disabledBlueButton; return ( {this.showSearchBar && !this.state.isLandscape ? : null} {this.showButtonsBar ? {this.showSearchBar && this.state.isLandscape ? : null} {showBackToCallButton ? : } : null} { !this.state.selectedContact ? { this.navigationRef = ref; }} data={this.navigationItems} extraData={this.state} keyExtractor={(item, index) => item.key} renderItem={this.renderNavigationItem} /> : null} {this.state.isTablet && 0? : null} ); } } ReadyBox.propTypes = { account : PropTypes.object, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, startCall : PropTypes.func.isRequired, startConference : PropTypes.func.isRequired, contacts : PropTypes.array, orientation : PropTypes.string, isTablet : PropTypes.bool, isLandscape : PropTypes.bool, refreshHistory : PropTypes.bool, refreshFavorites: PropTypes.bool, saveHistory : PropTypes.func, localHistory : PropTypes.array, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, toggleFavorite : PropTypes.func, myInvitedParties: PropTypes.object, toggleBlocked : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, defaultDomain : PropTypes.string, saveContact : PropTypes.func, selectContact : PropTypes.func, lookupContacts : PropTypes.func, call : PropTypes.object, goBackFunc : PropTypes.func, messages : PropTypes.object, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, confirmRead : PropTypes.func, deleteMessage : PropTypes.func, expireMessage : PropTypes.func, getMessages : PropTypes.func, deleteMessages : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, sendPublicKey : PropTypes.func, inviteContacts : PropTypes.bool, shareToContacts : PropTypes.bool, selectedContacts: PropTypes.array, updateSelection : PropTypes.func, loadEarlierMessages: PropTypes.func, newContactFunc : PropTypes.func, missedCalls : PropTypes.array, messageZoomFactor: PropTypes.string, isTyping: PropTypes.bool, navigationItems: PropTypes.object, showConferenceModal: PropTypes.bool, showConferenceModalFunc: PropTypes.func, hideConferenceModalFunc: PropTypes.func, shareContent: PropTypes.func, fetchSharedItems: PropTypes.func, filterHistoryFunc: PropTypes.func, historyFilter: PropTypes.string, fontScale: PropTypes.number, inviteToConferenceFunc: PropTypes.func, myContacts: PropTypes.object }; export default ReadyBox;