diff --git a/app/components/Call.js b/app/components/Call.js index 5391cbc..102d192 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,1367 +1,1371 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import assert from 'assert'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import uuid from 'react-native-uuid'; import AudioCallBox from './AudioCallBox'; import LocalMedia from './LocalMedia'; import VideoBox from './VideoBox'; import config from '../config'; import utils from '../utils'; import { ConnectionStateChangedEvent, ConnectionEventTypes, ProofAttributeInfo, ProofEventTypes, AttributeFilter } from '@aries-framework/core'; // Used for SSI credentials -const credDefId = 'EwAf16U6ZphXsZq6E5qmPz:2:Bloqzone_IDIN_ver5:5.0'; - +const credDefId = 'EwAf16U6ZphXsZq6E5qmPz:3:CL:394132:default'; function randomIntFromInterval(min,max) { return Math.floor(Math.random()*(max-min+1)+min); } function FixedQueue( size, initialValues ){ // If there are no initial arguments, default it to // an empty value so we can call the constructor in // a uniform way. initialValues = (initialValues || []); // Create the fixed queue array value. var queue = Array.apply( null, initialValues ); // Store the fixed size in the queue. queue.fixedSize = size; // Add the class methods to the queue. Some of these have // to override the native Array methods in order to make // sure the queue lenght is maintained. queue.push = FixedQueue.push; queue.splice = FixedQueue.splice; queue.unshift = FixedQueue.unshift; // Trim any initial excess from the queue. FixedQueue.trimTail.call( queue ); // Return the new queue. return( queue ); } // I trim the queue down to the appropriate size, removing // items from the beginning of the internal array. FixedQueue.trimHead = function(){ // Check to see if any trimming needs to be performed. if (this.length <= this.fixedSize){ // No trimming, return out. return; } // Trim whatever is beyond the fixed size. Array.prototype.splice.call( this, 0, (this.length - this.fixedSize) ); }; // I trim the queue down to the appropriate size, removing // items from the end of the internal array. FixedQueue.trimTail = function(){ // Check to see if any trimming needs to be performed. if (this.length <= this.fixedSize){ // No trimming, return out. return; } // Trim whatever is beyond the fixed size. Array.prototype.splice.call( this, this.fixedSize, (this.length - this.fixedSize) ); }; // I synthesize wrapper methods that call the native Array // methods followed by a trimming method. FixedQueue.wrapMethod = function( methodName, trimMethod ){ // Create a wrapper that calls the given method. var wrapper = function(){ // Get the native Array method. var method = Array.prototype[ methodName ]; // Call the native method first. var result = method.apply( this, arguments ); // Trim the queue now that it's been augmented. trimMethod.call( this ); // Return the original value. return( result ); }; // Return the wrapper method. return( wrapper ); }; // Wrap the native methods. FixedQueue.push = FixedQueue.wrapMethod( "push", FixedQueue.trimHead ); FixedQueue.splice = FixedQueue.wrapMethod( "splice", FixedQueue.trimTail ); FixedQueue.unshift = FixedQueue.wrapMethod( "unshift", FixedQueue.trimTail ); class Call extends Component { constructor(props) { super(props); autoBind(this); this.samples = 30; this.sampleInterval = 3; this.defaultWaitInterval = 90; // until we can connect or reconnect this.waitCounter = 0; this.waitInterval = this.defaultWaitInterval; this.videoBytesSent = 0; this.audioBytesSent = 0; this.videoBytesReceived = 0; this.audioBytesReceived = 0; this.packetLoss = 0; this.audioCodec = ''; this.videoCodec = ''; this.packetLossQueue = FixedQueue(this.samples); this.latencyQueue = FixedQueue(this.samples); this.audioBandwidthQueue = FixedQueue(this.samples); this.videoBandwidthQueue = FixedQueue(this.samples); this.mediaLost = false; this.ssiRoles = []; // can be holder, verifier and issuer this.ssiRemoteRoles = []; // can be holder, verifier or issuer let ssiRequired = false; this.cancelVerifyIdentityTimer = null; if (this.props.ssiRoles) { this.ssiRoles = this.props.ssiRoles; } let callUUID; let remoteUri = ''; let remoteDisplayName = ''; let callState = null; let direction = null; let callEnded = false; this.mediaIsPlaying = false; this.ended = false; this.answering = false; if (this.props.call) { // If current call is available on mount we must have incoming this.props.call.on('stateChanged', this.callStateChanged); this.props.call.on('incomingMessage', this.incomingMessage); remoteUri = this.props.call.remoteIdentity.uri; callState = this.props.call.state; remoteDisplayName = this.props.call.remoteIdentity.displayName || this.props.call.remoteIdentity.uri; direction = this.props.call.direction; callUUID = this.props.call.id; if (this.props.ssiAgent) { this.props.call.headers.forEach((header) => { if (header.name === 'SSI-roles') { this.ssiRemoteRoles = header.value.split(','); if (this.ssiRemoteRoles.indexOf('holder') > -1) { console.log('Remote party is an SSI holder'); if (this.ssiRoles.indexOf('verifier') === -1) { this.ssiRoles.push('verifier'); //we can verify the remote party } } if (this.ssiRemoteRoles.indexOf('issuer') > -1) { console.log('Remote party is an SSI issuer'); } if (this.ssiRemoteRoles.indexOf('verifier') > -1) { console.log('Remote party is an SSI verifier'); } } }); } } else { remoteUri = this.props.targetUri; remoteDisplayName = this.props.targetUri; callUUID = this.props.callUUID; direction = callUUID ? 'outgoing' : 'incoming'; ssiRequired = this.props.ssiRequired; } if (this.props.connection) { //console.log('Added listener for connection', this.props.connection); this.props.connection.on('stateChanged', this.connectionStateChanged); } let audioOnly = false; if (this.props.localMedia && this.props.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.state = { call: this.props.call, targetUri: this.props.targetUri, audioOnly: audioOnly, boo: false, remoteUri: remoteUri, remoteDisplayName: remoteDisplayName, localMedia: this.props.localMedia, connection: this.props.connection, accountId: this.props.account ? this.props.account.id : null, account: this.props.account, callState: callState, direction: direction, callUUID: callUUID, reconnectingCall: this.props.reconnectingCall, info: '', packetLossQueue: [], audioBandwidthQueue: [], videoBandwidthQueue: [], latencyQueue: [], declineReason: this.props.declineReason, messages: this.props.messages, selectedContact: this.props.selectedContact, callContact: this.props.callContact, selectedContacts: this.props.selectedContacts, ssiRequired: ssiRequired, ssiAgent: this.props.ssiAgent, ssiInvitationUrl: null, ssiRemoteIdentity: null, ssiVerified: null, ssiVerifyInProgress: false, ssiCanVerify: false } this.statisticsTimer = setInterval(() => { this.getConnectionStats(); }, this.sampleInterval * 1000); } componentDidMount() { if (this.props.ssiAgent) { this.props.ssiAgent.events.on(ConnectionEventTypes.ConnectionStateChanged, this.handleSSIAgentConnectionStateChange); this.props.ssiAgent.events.on(ProofEventTypes.ProofStateChanged, this.handleSSIAgentProofStateChange); } this.resetStats(); this.lookupContact(); if (this.state.direction === 'outgoing' && this.state.callUUID && this.state.callState !== 'established') { utils.timestampedLog('Call: start call', this.state.callUUID, 'when ready to', this.state.targetUri); this.startCallWhenReady(this.state.callUUID); } if (this.state.call === null) { this.mediaPlaying(); } } componentWillUnmount() { this.ended = true; this.answering = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); this.state.call.removeListener('incomingMessage', this.incomingMessage); } if (this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); } + + if (this.props.ssiAgent) { + this.props.ssiAgent.events.removeListener(ConnectionEventTypes.ConnectionStateChanged, this.handleSSIAgentConnectionStateChange); + this.props.ssiAgent.events.removeListener(ProofEventTypes.ProofStateChanged, this.handleSSIAgentProofStateChange); + } } async handleSSIAgentConnectionStateChange(event) { const connectionRecord = event.payload.connectionRecord; utils.timestampedLog('SSI session connection', connectionRecord.id, event.payload.previousState, '->', connectionRecord.state); //utils.timestampedLog('SSI connection event', connectionRecord); if (connectionRecord.state === 'responded' || connectionRecord.state === 'complete' && !this.state.ssiCanVerify) { this.setState({ssiCanVerify: true}); utils.timestampedLog('SSI connection established, we can now verify the remote party'); this.props.postSystemNotification('You may now verify the remote party'); } } async handleSSIAgentProofStateChange(event) { const proofRecord = event.payload.proofRecord; utils.timestampedLog('SSI proof event', proofRecord.id, 'new state:', proofRecord.state); //utils.timestampedLog('SSI proof event', proofRecord); if (this.ssiRoles.indexOf('verifier') > -1) { if (proofRecord.state === 'done') { this.cancelSSIVerify(); if (proofRecord.isVerified === undefined) { // the other party did the verification this.props.postSystemNotification('We were verified'); utils.timestampedLog('The other party verified our SSI credential'); } else if (proofRecord.isVerified === true) { // the verification was successful --> call is authorized const proofData = proofRecord.presentationMessage.presentationAttachments[0].getDataAsJson(); const proofValues = proofData.requested_proof.revealed_attr_groups.name.values; - const schemaId = proofData.identifiers[0].schema_id; + const _credDefId = proofData.identifiers[0].cred_def_id; - if (schemaId !== credDefId) { - utils.timestampedLog('SSI credentials schema', schemaId, 'is not supported'); - this.props.postSystemNotification('SSI credential schema ' + schemaId + ' is invalid'); + if (_credDefId !== credDefId) { + utils.timestampedLog('SSI credential definition id', _credDefId, 'is not supported'); + this.props.postSystemNotification('SSI credential id' + _credDefId + ' is invalid'); return; } const initials = proofValues.initials.raw; const legalName = proofValues.legalName.raw; const birthDate = proofValues.birthDate.raw; let verifiedDisplayName = initials + ' ' + legalName + ' (' + birthDate + ')'; utils.timestampedLog('SSI verify proof succeeded for:', verifiedDisplayName); const credentialAttributes = proofRecord.presentationMessage.indyProof; //console.log(credentialAttributes.proof.proofs); this.setState({ssiRemoteIdentity: credentialAttributes, ssiVerified: true, remoteDisplayName: verifiedDisplayName }); } else if (proofRecord.isVerified === false) { utils.timestampedLog('SSI verify proof failed'); this.setState({ssiVerified: false}); } else { console.log('Invalid proof record isVerified value', proofRecord.isVerified); } this.setState({ssiVerifyInProgress: false}); } } } cancelSSIVerify() { if (!this.cancelVerifyIdentityTimer) { return; } clearTimeout(this.cancelVerifyIdentityTimer); this.cancelVerifyIdentityTimer = null; this.setState({ssiVerifyInProgress: false}); } async verifySSIIdentity() { if (this.state.ssiConnectionRecord) { this.requestSSIProof(this.state.ssiConnectionRecord.id); } else { this.initSSIConnection(); } } async requestSSIProof(connectionRecordId) { if (this.state.ssiVerifyInProgress) { utils.timestampedLog('SSI proof verify in progress') } const attributes = { name: new ProofAttributeInfo({ names: ['legalName', 'initials', 'birthDate'], restrictions: [ new AttributeFilter({ credentialDefinitionId: credDefId }) ] }) } const proofRequestOptions = { name: "Proof Request Title", requestedAttributes: attributes } this.setState({ssiVerifyInProgress: true}); this.cancelSSIVerify(); this.cancelVerifyIdentityTimer = setTimeout(() => { this.cancelSSIVerify(); this.props.postSystemNotification('SSI proof request timeout'); }, 15000); try { utils.timestampedLog('Request SSI proof over connection', connectionRecordId, 'for schema', credDefId); const proofRequest = await this.state.ssiAgent.proofs.requestProof(connectionRecordId, proofRequestOptions); //console.log(proofRequestOptions); } catch (error) { utils.timestampedLog('SSI proof request error', error); this.props.postSystemNotification('SSI proof ' + error); this.setState({ssiVerifyInProgress: false}); } } async initSSIConnection() { if (!this.state.ssiAgent) { console.log('No SSI Agent available'); return; } if (this.state.ssiConnectionRecord) { utils.timestampedLog('SSI connection already active'); return; } try { utils.timestampedLog('Creating SSI connection...'); const ssiConnection = await this.state.ssiAgent.connections.createConnection(); const invitationUrl = ssiConnection.invitation.toUrl({domain: "http://example.com"}); this.setState({ssiInvitationUrl: invitationUrl, ssiConnectionRecord: ssiConnection.connectionRecord}); this.sendSSIInvitation(); } catch (error) { utils.timestampedLog('SSI create connection', error); this.props.postSystemNotification('SSI connection ' + error); this.setState({ssiVerified: false}); } } sendSSIInvitation() { if (!this.state.call) { return; } utils.timestampedLog('SSI invitation sent'); this.props.postSystemNotification('SSI invitation sent'); let message = this.state.call.sendMessage(this.state.ssiInvitationUrl, 'text/ssi-invitation-url', {id: uuid.v4()}, (error) => { if (error) { console.log('Message', id, 'sending error:', error); } }); } incomingMessage(message) { console.log('Session message', message.id, message.contentType, 'received'); if (message.contentType === 'text/ssi-invitation-url' && this.state.ssiAgent && !this.state.ssiInvitationUrl) { this.receiveSSIInvitation(message.content); } } async receiveSSIInvitation(url) { utils.timestampedLog('SSI received invitation URL', url); try { const ssiConnectionRecord = await this.state.ssiAgent.connections.receiveInvitationFromUrl(url); utils.timestampedLog('SSI invitation accepted', ssiConnectionRecord.id); this.props.postSystemNotification('SSI invitation accepted'); this.setState({ssiInvitationUrl: url, ssiConnectionRecord: ssiConnectionRecord}); } catch (error) { this.setState({ssiVerified: false}); utils.timestampedLog('SSI accept invitation error', error); this.props.postSystemNotification('SSI accept ' + error); } } resetStats() { if (this.ended) { return; } this.setState({ bandwidth: '', packetLossQueue: [], audioBandwidthQueue: [], videoBandwidthQueue: [], latencyQueue: [] }); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { // Needed for switching to incoming call while in a call if (this.ended) { return; } if (nextProps.connection && nextProps.connection !== this.state.connection) { nextProps.connection.on('stateChanged', this.connectionStateChanged); } this.setState({connection: nextProps.connection, account: nextProps.account, call: nextProps.call, callContact: nextProps.callContact, accountId: nextProps.account ? nextProps.account.id : null}); if (this.state.call === null && nextProps.call !== null) { nextProps.call.on('stateChanged', this.callStateChanged); nextProps.call.on('incomingMessage', this.incomingMessage); this.setState({ remoteUri: nextProps.call.remoteIdentity.uri, direction: nextProps.call.direction, callUUID: nextProps.call.id, remoteDisplayName: nextProps.call.remoteIdentity.displayName }); this.lookupContact(); } else { if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) { this.setState({'callUUID': nextProps.callUUID, 'direction': 'outgoing', 'call': null }); this.startCallWhenReady(nextProps.callUUID); } } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.targetUri !== this.state.targetUri && this.state.direction === 'outgoing') { this.setState({targetUri: nextProps.targetUri}); } this.setState({registrationState: nextProps.registrationState, declineReason: nextProps.declineReason}); if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia && this.state.direction === 'outgoing') { utils.timestampedLog('Call: media for outgoing call has been changed'); let audioOnly = false; if (nextProps.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.setState({localMedia: nextProps.localMedia, audioOnly: audioOnly}); //this.mediaPlaying(nextProps.localMedia); } if (nextProps.hasOwnProperty('ssiCanVerify')) { this.setState({ssiCanVerify: nextProps.ssiCanVerify}); } this.setState({messages: nextProps.messages, selectedContacts: nextProps.selectedContacts, ssiVerifyInProgress: nextProps.ssiVerifyInProgress }); if (nextProps.ssiConnectionRecord) { this.setState({ssiConnectionRecord: nextProps.ssiConnectionRecord}); } if (nextProps.ssiInvitationUrl) { this.setState({ssiInvitationUrl: nextProps.ssiInvitationUrl}); } } getConnectionStats() { if (this.ended) { return; } let speed = 0; let diff = 0; let delay = 0; let audioPackets = 0; let videoPackets = 0; let audioPacketsLost = 0; let videoPacketsLost = 0; let audioPacketLoss = 0; let videoPacketLoss = 0; let bandwidthUpload = 0; let bandwidthDownload = 0; let mediaType; let foundVideo = false; if (!this.state.call || !this.state.call._pc) { this.resetStats(); return; } this.state.call._pc.getStats(null).then(stats => { stats.forEach(report => { if (report.type === "ssrc") { report.values.forEach(object => { if (object.mediaType) { mediaType = object.mediaType; } }); report.values.forEach(object => { if (object.googCodecName) { if (mediaType === 'video') { this.audioCodec = object.googCodecName; } else { this.videoCodec = object.googCodecName; } } else if (object.bytesReceived) { const bytesReceived = Math.floor(object.bytesReceived); if (mediaType === 'audio') { if (this.audioBytesReceived > 0 && this.audioBytesReceived < bytesReceived) { diff = bytesReceived - this.audioBytesReceived; diff = bytesReceived - this.audioBytesReceived; speed = Math.floor(diff / this.sampleInterval * 8 / 1000); //console.log('Audio bandwidth received', speed, 'kbit/s'); bandwidthDownload = bandwidthDownload + speed; if (this.audioBandwidthQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.audioBandwidthQueue.push(0); n = n - 1; } } this.audioBandwidthQueue.push(speed); } this.audioBytesReceived = bytesReceived; } else if (mediaType === 'video') { foundVideo = true; if (this.videoBytesReceived > 0 && this.videoBytesReceived < bytesReceived) { diff = bytesReceived - this.videoBytesReceived; speed = Math.floor(diff / this.sampleInterval * 8 / 1000); //console.log('Video bandwidth received', speed, 'kbit/s'); bandwidthDownload = bandwidthDownload + speed; if (this.videoBandwidthQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.videoBandwidthQueue.push(0); n = n - 1; } } this.videoBandwidthQueue.push(speed) } this.videoBytesReceived = bytesReceived; } } else if (object.bytesSent) { const bytesSent = Math.floor(object.bytesSent); if (mediaType === 'audio') { if (this.audioBytesSent > 0 && bytesSent > this.audioBytesSent) { const diff = bytesSent - this.audioBytesSent; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log('Audio bandwidth sent', speed, 'kbit/s'); } this.audioBytesSent = bytesSent; } else if (mediaType === 'video') { foundVideo = true; if (this.videoBytesSent > 0 && bytesSent > this.videoBytesSent) { const diff = bytesSent - this.videoBytesSent; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log('Video bandwidth sent', speed, 'kbit/s'); } this.videoBytesSent = bytesSent; } } else if (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); } } else if (object.packetsReceived) { if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsReceived); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsReceived); } } else if (object.googCurrentDelayMs) { delay = object.googCurrentDelayMs; } //console.log(object); }); }}); // packet loss videoPacketLoss = 0; if (videoPackets > 0) { videoPacketLoss = Math.floor(videoPacketsLost / videoPackets * 100); if (videoPacketLoss > 1) { //console.log('Video packet loss', videoPacketLoss, '%'); } } audioPacketLoss = 0; if (audioPackets > 0) { audioPacketLoss = Math.floor(audioPacketsLost / audioPackets * 100); if (audioPacketLoss > 3) { //console.log('Audio packet loss', audioPacketLoss, '%'); } } this.packetLoss = videoPacketLoss > audioPacketLoss ? videoPacketLoss : audioPacketLoss; //this.packetLoss = randomIntFromInterval(2, 10); if (this.packetLoss < 3) { this.packetLoss = 0; } if (this.packetLossQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.packetLossQueue.push(0); n = n - 1; } } if (this.latencyQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.latencyQueue.push(0); n = n - 1; } } this.latencyQueue.push(Math.ceil(delay)); this.packetLossQueue.push(this.packetLoss); this.audioPacketLoss = audioPacketLoss; this.videoPacketLoss = videoPacketLoss; let info = ''; let suffix = 'kbit/s'; if (foundVideo && (bandwidthUpload > 0 || bandwidthDownload > 0)) { suffix = 'Mbit/s'; bandwidthUpload = Math.ceil(bandwidthUpload / 1000 * 100) / 100; bandwidthDownload = Math.ceil(bandwidthDownload / 1000 * 100) / 100; } if (bandwidthDownload && bandwidthUpload) { if (bandwidthDownload > 0 && bandwidthUpload > 0) { info = '⇣' + bandwidthDownload + ' ⇡' + bandwidthUpload; } else if (bandwidthDownload > 0) { info = '⇣' + bandwidthDownload; } else if (bandwidthUpload > 0) { info = '⇡' + this.bandwidthUpload; } if (info) { info = info + ' ' + suffix; } } if (this.packetLoss > 2) { info = info + ' - ' + Math.ceil(this.packetLoss) + '% loss'; } if (delay > 150) { info = info + ' - ' + Math.ceil(delay) + ' ms'; } this.setState({packetLossQueue: this.packetLossQueue, latencyQueue: this.latencyQueue, videoBandwidthQueue: this.videoBandwidthQueue, audioBandwidthQueue: this.audioBandwidthQueue, info: info }); }); }; mediaPlaying(localMedia) { console.log('Media playing'); if (this.state.direction === 'incoming') { const media = localMedia ? localMedia : this.state.localMedia; this.answerCall(media); } else { this.mediaIsPlaying = true; } } async answerCall(localMedia) { const media = localMedia ? localMedia : this.state.localMedia; if (this.state.call && this.state.call.state === 'incoming' && media) { let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = media; if (this.state.ssiAgent) { options.headers = [{name: 'SSI-roles', value: this.ssiRoles.toString()}]; //console.log('Call answer extra headers:', options.headers); } if (!this.answering) { this.answering = true; const connectionState = this.state.connection.state ? this.state.connection.state : null; utils.timestampedLog('Call: answering call', this.state.call.id, 'in connection state', connectionState); try { this.state.call.answer(options); utils.timestampedLog('Call: answered'); } catch (error) { utils.timestampedLog('Call: failed to answer', error); this.hangupCall('answer_failed') } } else { utils.timestampedLog('Call: answering call in progress...'); } } else { if (!this.state.call) { utils.timestampedLog('Call: no Sylkrtc call present'); this.hangupCall('answer_failed'); } if (this.state.call && this.state.call.state !== 'incoming') { utils.timestampedLog('Call: state is not incoming'); } if (!media) { utils.timestampedLog('Call: waiting for local media'); } } } lookupContact() { // TODO this must lookup in myContacts let photo = null; let remoteUri = this.state.remoteUri || ''; let remoteDisplayName = this.state.remoteDisplayName || ''; if (!remoteUri) { return; } if (remoteUri.indexOf('3333@') > -1) { remoteDisplayName = 'Video Test'; } else if (remoteUri.indexOf('4444@') > -1) { remoteDisplayName = 'Echo Test'; } else if (this.props.myContacts.hasOwnProperty(remoteUri) && this.props.myContacts[remoteUri].name) { remoteDisplayName = this.props.myContacts[remoteUri].name; } else if (this.props.contacts) { let username = remoteUri.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', remoteUri); } if (contact_obj) { remoteDisplayName = contact_obj.displayName; photo = contact_obj.photo; if (isPhoneNumber) { remoteUri = username; } } else { if (isPhoneNumber) { remoteUri = username; remoteDisplayName = username; } } } this.setState({remoteDisplayName: remoteDisplayName, remoteUri: remoteUri, photo: photo }); } callStateChanged(oldState, newState, data) { //console.log('Call: callStateChanged', oldState, '->', newState); if (this.ended) { return; } let remoteHasNoVideoTracks; let remoteIsRecvOnly; let remoteIsInactive; let remoteStreams; this.answering = false; if (newState === 'established') { this.setState({reconnectingCall: false}); const currentCall = this.state.call; if (this.state.direction === 'outgoing' && this.ssiRemoteRoles.length > 0) { this.initSSIConnection(); } if (this.state.direction === 'outgoing') { if (this.ssiRemoteRoles.length > 0) { utils.timestampedLog('SSI local roles:', this.ssiRoles.toString()); utils.timestampedLog('SSI remote roles:', this.ssiRemoteRoles.toString()); } else { console.log('Remove party does not support SSI'); } } else { if (this.ssiRemoteRoles.length > 0) { utils.timestampedLog('SSI local roles:', this.ssiRoles.toString()); utils.timestampedLog('SSI remote roles:', this.ssiRemoteRoles.toString()); } else { console.log('Remove party does not support SSI'); } } if (currentCall) { remoteStreams = currentCall.getRemoteStreams(); if (remoteStreams) { if (remoteStreams.length > 0) { const remotestream = remoteStreams[0]; remoteHasNoVideoTracks = remotestream.getVideoTracks().length === 0; remoteIsRecvOnly = currentCall.remoteMediaDirections.video[0] === 'recvonly'; remoteIsInactive = currentCall.remoteMediaDirections.video[0] === 'inactive'; } } } if (remoteStreams && (remoteHasNoVideoTracks || remoteIsRecvOnly || remoteIsInactive) && !this.state.audioOnly) { //console.log('Media type changed to audio'); // Stop local video if (this.state.localMedia.getVideoTracks().length !== 0) { currentCall.getLocalStreams()[0].getVideoTracks()[0].stop(); } this.setState({audioOnly: true}); } else { this.forceUpdate(); } } else if (newState === 'accepted') { // Switch if we have audioOnly and local videotracks. This means // the call object switched and we are transitioning to an // incoming call. if (this.state.audioOnly && this.state.localMedia && this.state.localMedia.getVideoTracks().length !== 0) { //console.log('Media type changed to video on accepted'); this.setState({audioOnly: false}); } data.headers.forEach((header) => { if (header.name === 'SSI-roles') { this.ssiRemoteRoles = header.value.split(','); if (this.ssiRemoteRoles.indexOf('holder') > -1) { utils.timestampedLog('Remote party is an SSI holder'); } if (this.ssiRemoteRoles.indexOf('issuer') > -1) { utils.timestampedLog('Remote party is an SSI issuer'); } if (this.ssiRemoteRoles.indexOf('verifier') > -1) { utils.timestampedLog('Remote party is an SSI verifier'); } } }); } if (newState !== 'established') { this.cancelSSIVerify(); } this.forceUpdate(); } connectionStateChanged(oldState, newState) { switch (newState) { case 'closed': break; case 'ready': break; case 'disconnected': if (oldState === 'ready' && this.state.direction === 'outgoing') { utils.timestampedLog('Call: reconnecting the call...'); this.waitInterval = this.defaultWaitInterval; } break; default: break; } } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } canConnect() { if (!this.state.connection) { console.log('Call: no connection yet'); return false; } if (this.state.connection.state !== 'ready') { console.log('Call: connection is not ready'); return false; } if (this.props.registrationState !== 'registered') { console.log('Call: account not ready yet'); return false; } if (!this.mediaIsPlaying) { if (this.waitCounter > 0) { console.log('Call: media is not yet playing'); } return false; } return true; } async startCallWhenReady(callUUID) { this.waitCounter = 0; let diff = 0; while (this.waitCounter < this.waitInterval) { if (this.waitCounter === 1) { utils.timestampedLog('Call: waiting for establishing call', this.waitInterval, 'seconds'); } if (this.userHangup) { this.hangupCall('user_cancelled'); return; } if (this.ended) { return; } if (this.waitCounter >= this.waitInterval - 1) { this.hangupCall('timeout'); } if (!this.canConnect()) { //utils.timestampedLog('Call: waiting for connection', this.waitInterval - this.waitCounter, 'seconds'); if (this.state.call && this.state.call.id === callUUID && this.state.call.state !== 'terminated') { return; } if (this.waitCounter > 0 && this.waitCounter % 10 === 0) { console.log('Wait', this.waitCounter); } await this._sleep(1000); } else { this.waitCounter = 0; this.start(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } start() { if (this.state.localMedia === null) { console.log('Call: cannot create new call without local media'); return; } let options = { pcConfig: {iceServers: config.iceServers}, id: this.state.callUUID, localStream: this.state.localMedia }; if (this.state.ssiAgent) { options.headers = [{name: 'SSI-roles', value: this.ssiRoles.toString()}]; //console.log('Outgoing call extra headers:', options.headers); } let call = this.state.account.call(this.state.targetUri, options); this.setState({call: call}); } hangupCall(reason) { let callUUID = this.state.call ? this.state.call.id : this.state.callUUID; this.waitInterval = this.defaultWaitInterval; if (this.state.call) { //console.log('Remove listener for call', this.state.call.id); this.state.call.removeListener('stateChanged', this.callStateChanged); this.state.call.removeListener('incomingMessage', this.incomingMessage); this.setState({call: null}); } if (this.state.connection) { //console.log('Remove listener for connection', this.state.connection); this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.setState({connection: null}); } if (this.waitCounter > 0) { this.waitCounter = this.waitInterval; } this.props.hangupCall(callUUID, reason); } render() { let box = null; if (this.state.localMedia !== null) { if (this.state.audioOnly) { box = ( ); } else { if (this.state.call !== null && (this.state.call.state === 'established' || (this.state.call.state === 'terminated' && this.state.reconnectingCall))) { box = ( ); } else { if (this.state.call && this.state.call.state === 'terminated' && this.state.reconnectingCall) { //console.log('Skip render local media because we will reconnect'); } else { box = ( ); } } } } else { box = ( ); } return box; } } Call.propTypes = { targetUri : PropTypes.string, account : PropTypes.object, hangupCall : PropTypes.func, connection : PropTypes.object, registrationState : PropTypes.string, call : PropTypes.object, localMedia : PropTypes.object, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, callUUID : PropTypes.string, contacts : PropTypes.array, intercomDtmfTone : PropTypes.string, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool, myContacts : PropTypes.object, declineReason : PropTypes.string, showLogs : PropTypes.func, goBackFunc : PropTypes.func, callState : PropTypes.object, messages : PropTypes.object, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, confirmRead : PropTypes.func, deleteMessage : PropTypes.func, expireMessage : PropTypes.func, getMessages : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, selectedContact : PropTypes.object, callContact : PropTypes.object, selectedContacts : PropTypes.array, inviteToConferenceFunc : PropTypes.func, finishInvite : PropTypes.func, ssiRequired : PropTypes.bool, ssiAgent : PropTypes.object, ssiRoles : PropTypes.array, postSystemNotification : PropTypes.func }; export default Call; diff --git a/app/components/ContactsListBox.js b/app/components/ContactsListBox.js index ef3c873..ab9b6e7 100644 --- a/app/components/ContactsListBox.js +++ b/app/components/ContactsListBox.js @@ -1,1352 +1,1345 @@ import React, { Component} from 'react'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import { Clipboard, SafeAreaView, View, FlatList, Text, Linking, PermissionsAndroid, Switch} from 'react-native'; import ContactCard from './ContactCard'; import utils from '../utils'; import DigestAuthRequest from 'digest-auth-request'; import uuid from 'react-native-uuid'; import { GiftedChat, IMessage, Bubble, MessageText } from 'react-native-gifted-chat' import MessageInfoModal from './MessageInfoModal'; import ShareMessageModal from './ShareMessageModal'; import CustomChatActions from './ChatActions'; import FileViewer from 'react-native-file-viewer'; import moment from 'moment'; import momenttz from 'moment-timezone'; //import Video from 'react-native-video'; const RNFS = require('react-native-fs'); import CameraRoll from "@react-native-community/cameraroll"; import styles from '../assets/styles/blink/_ContactsListBox.scss'; String.prototype.toDate = function(format) { var normalized = this.replace(/[^a-zA-Z0-9]/g, '-'); var normalizedFormat= format.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-'); var formatItems = normalizedFormat.split('-'); var dateItems = normalized.split('-'); var monthIndex = formatItems.indexOf("mm"); var dayIndex = formatItems.indexOf("dd"); var yearIndex = formatItems.indexOf("yyyy"); var hourIndex = formatItems.indexOf("hh"); var minutesIndex = formatItems.indexOf("ii"); var secondsIndex = formatItems.indexOf("ss"); var today = new Date(); var year = yearIndex>-1 ? dateItems[yearIndex] : today.getFullYear(); var month = monthIndex>-1 ? dateItems[monthIndex]-1 : today.getMonth()-1; var day = dayIndex>-1 ? dateItems[dayIndex] : today.getDate(); var hour = hourIndex>-1 ? dateItems[hourIndex] : today.getHours(); var minute = minutesIndex>-1 ? dateItems[minutesIndex] : today.getMinutes(); var second = secondsIndex>-1 ? dateItems[secondsIndex] : today.getSeconds(); return new Date(year,month,day,hour,minute,second); }; class ContactsListBox extends Component { constructor(props) { super(props); autoBind(this); this.chatListRef = React.createRef(); let renderMessages = []; if (this.props.selectedContact) { let uri = this.props.selectedContact.uri; if (uri in this.props.messages) { renderMessages = this.props.messages[uri]; //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 }); } } this.state = { accountId: this.props.account ? this.props.account.id : null, password: this.props.password, targetUri: this.props.selectedContact ? this.props.selectedContact.uri : this.props.targetUri, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, isRefreshing: false, isLandscape: this.props.isLandscape, contacts: this.props.contacts, myInvitedParties: this.props.myInvitedParties, refreshHistory: this.props.refreshHistory, selectedContact: this.props.selectedContact, myContacts: this.props.myContacts, messages: this.props.messages, renderMessages: GiftedChat.append(renderMessages, []), chat: this.props.chat, pinned: false, showMessageModal: false, message: null, showShareMessageModal: false, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectedContacts: this.props.selectedContacts, pinned: this.props.pinned, filter: this.props.filter, periodFilter: this.props.periodFilter, scrollToBottom: true, messageZoomFactor: this.props.messageZoomFactor, isTyping: false, isLoadingEarlier: false, fontScale: this.props.fontScale, call: this.props.call, isTablet: this.props.isTablet, ssiCredentials: this.props.ssiCredentials, ssiConnections: this.props.ssiConnections } this.ended = false; } componentDidMount() { this.ended = false; } componentWillUnmount() { this.ended = true; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (this.ended) { return; } if (nextProps.myInvitedParties !== this.state.myInvitedParties) { this.setState({myInvitedParties: nextProps.myInvitedParties}); } if (nextProps.contacts !== this.state.contacts) { this.setState({contacts: nextProps.contacts}); } if (nextProps.favoriteUris !== this.state.favoriteUris) { this.setState({favoriteUris: nextProps.favoriteUris}); } if (nextProps.blockedUris !== this.state.blockedUris) { this.setState({blockedUris: nextProps.blockedUris}); } if (nextProps.account !== null && nextProps.account !== this.props.account) { this.setState({accountId: nextProps.account.id}); } if (nextProps.refreshHistory !== this.state.refreshHistory) { this.setState({refreshHistory: nextProps.refreshHistory}); this.getServerHistory(); } if (nextProps.messageZoomFactor !== this.state.messageZoomFactor) { this.setState({scrollToBottom: false, messageZoomFactor: nextProps.messageZoomFactor}); } if (nextProps.selectedContact !== this.state.selectedContact) { //console.log('Selected contact changed to', nextProps.selectedContact); this.setState({selectedContact: nextProps.selectedContact}); if (nextProps.selectedContact) { this.setState({scrollToBottom: true}); if (Object.keys(this.state.messages).indexOf(nextProps.selectedContact.uri) === -1) { this.props.getMessages(nextProps.selectedContact.uri); } } else { this.setState({renderMessages: []}); } }; if (nextProps.myContacts !== this.state.myContacts) { this.setState({myContacts: nextProps.myContacts}); }; if (nextProps.selectedContact) { let renderMessages = []; let uri = nextProps.selectedContact.uri; if (uri in nextProps.messages) { renderMessages = nextProps.messages[uri]; if (this.state.renderMessages.length !== renderMessages.length) { this.setState({isLoadingEarlier: false}); this.props.confirmRead(uri); if (this.state.renderMessages.length > 0 && renderMessages.length > 0) { let last_message_ts = this.state.renderMessages[0].createdAt; if (renderMessages[0].createdAt > last_message_ts) { this.setState({scrollToBottom: true}); } } } } if (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 }); this.setState({renderMessages: GiftedChat.append(renderMessages, [])}); if (!this.state.scrollToBottom && renderMessages.length > 0) { //console.log('Scroll to first message'); //this.scrollToMessage(0); } } } this.setState({isLandscape: nextProps.isLandscape, isTablet: nextProps.isTablet, chat: nextProps.chat, fontScale: nextProps.fontScale, filter: nextProps.filter, call: nextProps.call, password: nextProps.password, showMessageModal: nextProps.showMessageModal, messages: nextProps.messages, inviteContacts: nextProps.inviteContacts, shareToContacts: nextProps.shareToContacts, selectedContacts: nextProps.selectedContacts, pinned: nextProps.pinned, isTyping: nextProps.isTyping, periodFilter: nextProps.periodFilter, ssiCredentials: nextProps.ssiCredentials, ssiConnections: nextProps.ssiConnections, targetUri: nextProps.selectedContact ? nextProps.selectedContact.uri : nextProps.targetUri }); if (nextProps.isTyping) { setTimeout(() => { this.setState({isTyping: false}); }, 3000); } } renderCustomActions = props => ( ) onSendFromUser() { console.log('On send from user...'); } setTargetUri(uri, contact) { //console.log('Set target uri uri in history list', uri); this.props.setTargetUri(uri, contact); } setFavoriteUri(uri) { return this.props.setFavoriteUri(uri); } setBlockedUri(uri) { return this.props.setBlockedUri(uri); } renderItem(object) { let item = object.item || object; let invitedParties = []; let uri = item.uri; let myDisplayName; let username = uri.split('@')[0]; if (this.state.myContacts && this.state.myContacts.hasOwnProperty(uri)) { myDisplayName = this.state.myContacts[uri].name; } if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(username)) { invitedParties = this.state.myInvitedParties[username]; } if (myDisplayName) { if (item.name === item.uri || item.name !== myDisplayName) { item.name = myDisplayName; } } return( ); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } closeMessageModal() { this.setState({showMessageModal: false, message: null}); } loadEarlierMessages() { //console.log('Load earlier messages...'); this.setState({scrollToBottom: false, isLoadingEarlier: true}); this.props.loadEarlierMessages(); } onSendWithFile(selectedFile) { let uri; if (!this.state.selectedContact) { if (this.state.targetUri && this.state.chat) { let contacts = this.searchedContact(this.state.targetUri); if (contacts.length !== 1) { return; } uri = contacts[0].uri; } else { return; } } else { uri = this.state.selectedContact.uri; } let fileData = { name: selectedFile.name, type: selectedFile.type, size: selectedFile.size, uri: selectedFile.uri }; console.log('Sending file', fileData); //this.props.sendMessage(uri, message); } onSendMessage(messages) { let uri; if (!this.state.selectedContact) { if (this.state.targetUri && this.state.chat) { let contacts = this.searchedContact(this.state.targetUri); if (contacts.length !== 1) { return; } uri = contacts[0].uri; } else { return; } } else { uri = this.state.selectedContact.uri; } messages.forEach((message) => { /* sent: true, // Mark the message as received, using two tick received: true, // Mark the message as pending with a clock loader pending: true, */ message.encrypted = this.state.selectedContact && this.state.selectedContact.publicKey ? 2 : 0; this.props.sendMessage(uri, message); }); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, messages)}); } searchedContact(uri, contact=null) { if (uri.indexOf(' ') > -1) { return []; } const item = this.props.newContactFunc(uri.toLowerCase(), null, {src: 'search_contact'}); if (!item) { return []; } if (contact) { item.name = contact.name; item.photo = contact.photo; } return [item]; } getServerHistory() { if (!this.state.accountId) { return; } if (this.ended || !this.state.accountId || this.state.isRefreshing) { return; } //console.log('Get server history...'); this.setState({isRefreshing: true}); let history = []; let localTime; let getServerCallHistory = new DigestAuthRequest( 'GET', `${this.props.config.serverCallHistoryUrl}?action=get_history&realm=${this.state.accountId.split('@')[1]}`, this.state.accountId.split('@')[0], this.state.password ); // Disable logging getServerCallHistory.loggingOn = false; getServerCallHistory.request((data) => { if (data.success !== undefined && data.success === false) { console.log('Error getting call history from server', data.error_message); return; } if (data.received) { data.received.map(elem => {elem.direction = 'incoming'; return elem}); history = history.concat(data.received); } if (data.placed) { data.placed.map(elem => {elem.direction = 'outgoing'; return elem}); history = history.concat(data.placed); } history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) if (history) { const known = []; history = history.filter((elem) => { elem.conference = false; elem.id = uuid.v4(); if (!elem.tags) { elem.tags = []; } if (elem.remoteParty.indexOf('@conference.') > -1) { return null; } elem.uri = elem.remoteParty.toLowerCase(); let uri_els = elem.uri.split('@'); let username = uri_els[0]; let domain; if (uri_els.length > 1) { domain = uri_els[1]; } if (elem.uri.indexOf('@guest.') > -1) { if (!elem.displayName) { elem.uri = 'guest@' + elem.uri.split('@')[1]; } else { elem.uri = elem.displayName.toLowerCase().replace(/\s|\-|\(|\)/g, '') + '@' + elem.uri.split('@')[1]; } } if (utils.isPhoneNumber(elem.uri)) { username = username.replace(/\s|\-|\(|\)/g, ''); username = username.replace(/^00/, "+"); elem.uri = username; } if (known.indexOf(elem.uri) > -1) { return null; } known.push(elem.uri); if (elem.displayName) { elem.name = elem.displayName; } else { elem.name = elem.uri; } if (elem.remoteParty.indexOf('@videoconference.') > -1) { elem.conference = true; elem.media = ['audio', 'video', 'chat']; } if (elem.uri === this.state.accountId) { elem.name = this.props.myDisplayName || 'Myself'; } if (!elem.media || !Array.isArray(elem.media)) { elem.media = ['audio']; } if (elem.timezone !== undefined) { localTime = momenttz.tz(elem.startTime, elem.timezone).toDate(); elem.startTime = localTime; elem.timestamp = localTime; localTime = momenttz.tz(elem.stopTime, elem.timezone).toDate(); elem.stopTime = localTime; } if (elem.direction === 'incoming' && elem.duration === 0) { elem.tags.push('missed'); } return elem; }); this.props.saveHistory(history); if (this.ended) { return; } this.setState({isRefreshing: false}); } }, (errorCode) => { console.log('Error getting call history from server', errorCode); }); this.setState({isRefreshing: false}); } matchContact(contact, filter='', tags=[]) { if (!contact) { return false; } if (tags.indexOf('conference') > -1 && contact.conference) { return true; } if (tags.length > 0 && !tags.some(item => contact.tags.includes(item))) { return false; } if (contact.name && contact.name.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return true; } if (contact.uri.toLowerCase().startsWith(filter.toLowerCase())) { return true; } if (!this.state.selectedContact && contact.conference && contact.metadata && filter.length > 2 && contact.metadata.indexOf(filter) > -1) { return true; } return false; } noChatInputToolbar () { return null; } onLongMessagePress(context, currentMessage) { if (currentMessage && currentMessage.text) { let isSsiMessage = this.state.selectedContact && this.state.selectedContact.tags.indexOf('ssi') > -1; let options = [] options.push('Copy'); if (!isSsiMessage) { options.push('Delete'); } const showResend = currentMessage.failed; if (this.state.targetUri.indexOf('@videoconference') === -1) { if (currentMessage.direction === 'outgoing') { if (showResend) { options.push('Resend') } } } if (currentMessage.pinned) { options.push('Unpin'); } else { if (!isSsiMessage) { options.push('Pin'); } } options.push('Info'); if (!isSsiMessage) { options.push('Share'); } if (currentMessage.local_url) { if (utils.isImage(currentMessage.local_url)) { options.push('Save'); } options.push('Open'); } options.push('Cancel'); 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 === 'Delete') { this.props.deleteMessage(currentMessage._id, this.state.targetUri); } else if (action === 'Pin') { this.props.pinMessage(currentMessage._id); } else if (action === 'Unpin') { this.props.unpinMessage(currentMessage._id); } else if (action === 'Info') { this.setState({message: currentMessage, showMessageModal: true}); } else if (action === 'Share') { this.setState({message: currentMessage, showShareMessageModal: true}); } else if (action === 'Resend') { this.props.reSendMessage(currentMessage, this.state.targetUri); } else if (action === 'Save') { this.savePicture(currentMessage.local_url); } else if (action === 'Open') { FileViewer.open(currentMessage.local_url, { showOpenWithDialog: true }) .then(() => { // success }) .catch(error => { // error }); } }); } }; async hasAndroidPermission() { const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE; const hasPermission = await PermissionsAndroid.check(permission); if (hasPermission) { return true; } const status = await PermissionsAndroid.request(permission); return status === 'granted'; } async savePicture(file) { if (Platform.OS === "android" && !(await this.hasAndroidPermission())) { return; } file = 'file://' + file; console.log('Save to camera roll', file); CameraRoll.save(file); }; shouldUpdateMessage(props, nextProps) { return true; } toggleShareMessageModal() { this.setState({showShareMessageModal: !this.state.showShareMessageModal}); } renderMessageVideo(props){ const { currentMessage } = props; return (null); return ( ); }; videoError() { console.log('Video streaming error'); } onBuffer() { console.log('Video buffer error'); } renderMessageText(props) { const {currentMessage} = props; const { text: currText } = currentMessage; let status = ''; let label = 'Uploading...'; if (!currentMessage.metadata) { return ( ); } if (currentMessage.direction === 'incoming') { label = 'Downloading...'; status = currentMessage.url; } else { status = currentMessage.url; } return ( ); }; renderMessageText(props) { return ( ); }; 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 ( ) } scrollToMessage(id) { //console.log('scrollToMessage', id); //https://github.com/FaridSafi/react-native-gifted-chat/issues/938 this.chatListRef.current?._messageContainerRef?.current?.scrollToIndex({ animated: true, index: id }); } get showChat() { if (this.state.selectedContact) { if (this.state.selectedContact.tags && this.state.selectedContact.tags.indexOf('blocked') > -1) { return false; } if (this.state.selectedContact.uri.indexOf('@guest.') > -1) { return false; } if (this.state.selectedContact.uri.indexOf('anonymous@') > -1) { return false; } } let username = this.state.targetUri ? this.state.targetUri.split('@')[0] : null; let isPhoneNumber = username ? username.match(/^(\+|0)(\d+)$/) : false; if (isPhoneNumber) { return false; } if (this.props.selectedContact) { return true; } return false; } ssi2GiftedChat(from_uri, content, timestamp) { let id = uuid.v4(); let msg; msg = { _id: id, key: id, text: content, createdAt: timestamp, direction: 'incoming', sent: false, received: true, pending: false, system: false, failed: false, user: {_id: from_uri, name: from_uri} } return msg; } getSsiContacts() { //console.log('Get SSI contacts'); let contacts = []; if (this.state.ssiCredentials) { this.state.ssiCredentials.forEach((item) => { let contact = this.props.newContactFunc(item.id, 'Credential'); contact.ssiCredential = item; contact.credential = new Object(); const schemaId = item.metadata.data['_internal/indyCredential'].schemaId; if (schemaId === 'EwAf16U6ZphXsZq6E5qmPz:2:Bloqzone_IDIN_ver5:5.0') { contact.schemaId = schemaId; item.credentialAttributes.forEach((attribute) => { contact.credential[attribute.name] = attribute.value; if (attribute.value.length > 0) { if (attribute.name === 'legalName') { contact.name = attribute.value; } else if (attribute.name === 'acceptDateTime') { contact.timestamp = attribute.value.toDate("dd-mm-yy hh:ii:ss"); } else if (attribute.name === 'createdAt') { contact.timestamp = attribute.value; } else if (attribute.name === 'emailAddress') { contact.email = attribute.value; } } }); } if (contact.credential.initials) { contact.name = contact.credential.initials; } if (contact.credential.legalName) { contact.name = contact.name + ' ' + contact.credential.legalName; } if (contact.credential.dob) { contact.name = contact.name + ' (' + contact.credential.dob + ')'; } if (contact.credential.birthDate) { contact.name = contact.name + ' (' + contact.credential.birthDate + ')'; } if (contact.credential.acceptDateTime && item.state === 'done') { contact.lastMessage = 'Credential issued at ' + contact.credential.acceptDateTime + ' (' + item.state + ')'; } contact.tags.push('ssi'); contact.tags.push('ssi-credential'); contact.tags.push('readonly'); contacts.push(contact); }); } if (this.state.ssiConnections) { this.state.ssiConnections.forEach((item) => { //console.log('Contacts SSI connection', item); let uri = item.id; let contact = this.props.newContactFunc(uri, item.theirLabel); contact.credential = new Object(); contact.timestamp = item.createdAt; contact.lastMessage = 'Connection is in state ' + item.state; contact.tags.push('ssi'); contact.tags.push('ssi-connection'); - if (item.theirLabel === 'Bloqzone Mediator Agent' && item.state === 'complete') { - contact.tags.push('readonly'); - } - - if (item.theirLabel === 'Bloqzone Issuer Agent' && item.state === 'complete') { - //contact.tags.push('readonly'); - } contact.ssiConnection = item; contacts.push(contact); }); } return contacts; } render() { let searchExtraItems = []; let items = []; let matchedContacts = []; let ssiContacts = []; let messages = this.state.renderMessages; let contacts = []; //console.log('--- Render contacts with filter', this.state.filter); //console.log('--- Render contacts', this.state.selectedContact); if (this.state.filter === 'ssi') { contacts = this.getSsiContacts(); } else { Object.keys(this.state.myContacts).forEach((uri) => { contacts.push(this.state.myContacts[uri]); }); } let chatInputClass; if (this.state.selectedContact) { if (this.state.selectedContact.uri.indexOf('@videoconference') > -1) { chatInputClass = this.noChatInputToolbar; } } else if (!this.state.chat) { chatInputClass = this.noChatInputToolbar; } if (!this.state.selectedContact && this.state.filter) { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri, [this.state.filter])); } else { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri)); searchExtraItems = searchExtraItems.concat(this.state.contacts); if (this.state.targetUri && this.state.targetUri.length > 2 && !this.state.selectedContact && !this.state.inviteContacts) { matchedContacts = searchExtraItems.filter(contact => this.matchContact(contact, this.state.targetUri)); } else if (this.state.selectedContact && this.state.selectedContact.type === 'contact') { matchedContacts.push(this.state.selectedContact); } else if (this.state.selectedContact) { items = [this.state.selectedContact]; } items = items.concat(matchedContacts); } if (this.state.targetUri) { items = items.concat(this.searchedContact(this.state.targetUri, this.state.selectedContact)); } if (this.state.filter && this.state.targetUri) { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri)); } const known = []; items = items.filter((elem) => { if (this.state.shareToContacts && elem.tags.indexOf('test') > -1) { return; } if (this.state.inviteContacts && elem.tags.indexOf('conference') > -1 ) { return; } if (this.state.shareToContacts && elem.tags.indexOf('chat') === -1) { return; } if (this.state.shareToContacts && elem.uri === this.state.accountId) { return; } if (this.state.accountId === elem.uri && elem.tags.length === 0) { return; } if (this.state.shareToContacts && elem.uri.indexOf('@') === -1) { return; } if (known.indexOf(elem.uri) <= -1) { known.push(elem.uri); return elem; } }); items.forEach((item) => { item.showActions = false; if (item.uri.indexOf('@videoconference.') === -1) { item.conference = false; } else { item.conference = true; } if (this.state.selectedContacts && this.state.selectedContacts.indexOf(item.uri) > -1) { item.selected = true; } else { item.selected = false; } }); let filteredItems = []; items.reverse(); var todayStart = new Date(); todayStart.setHours(0,0,0,0); var yesterdayStart = new Date(); yesterdayStart.setDate(todayStart.getDate() - 2); yesterdayStart.setHours(0,0,0,0); items.forEach((item) => { const fromDomain = '@' + item.uri.split('@')[1]; if (this.state.periodFilter === 'today') { if(item.timestamp < todayStart) { return; } } if (item.uri === 'anonymous@anonymous.invalid' && this.state.filter !== 'blocked') { return; } if (this.state.periodFilter === 'yesterday') { if(item.timestamp < yesterdayStart || item.timestamp > todayStart) { return; } } if (this.state.inviteContacts && item.uri.indexOf('@videoconference.') > -1) { return; } if (item.uri === this.state.accountId && !item.direction) { return; } if (this.state.filter && item.tags.indexOf(this.state.filter) > -1) { filteredItems.push(item); } else if (this.state.blockedUris.indexOf(item.uri) === -1 && this.state.blockedUris.indexOf(fromDomain) === -1) { filteredItems.push(item); } //console.log(item.timestamp, item.type, item.uri); }); items = filteredItems; items.sort((a, b) => (a.timestamp < b.timestamp) ? 1 : -1) if (items.length === 1) { items[0].showActions = true; if (items[0].tags.indexOf('ssi-credential') > -1) { let content = ''; let m; chatInputClass = this.noChatInputToolbar; items[0].ssiCredential.credentialAttributes.forEach((attribute) => { content = content + attribute.name + ": " + attribute.value + '\n'; }); m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); m = this.ssi2GiftedChat(items[0].uri, 'SSI credential body' , items[0].timestamp); m.system = true; messages.push(m); content = ''; content = content + 'Id: ' + items[0].ssiCredential.id; content = content + '\nState: ' + items[0].ssiCredential.state; content = content + '\nSchema Id:' + items[0].schemaId; let issuer = this.state.ssiConnections.filter(x => x.id === items[0].ssiCredential.connectionId); if (issuer.length === 1) { content = content + '\nIssuer: ' + issuer[0].theirLabel; } else { content = content + '\nIssuer: : ' + items[0].ssiCredential.connectionId; } m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); m = this.ssi2GiftedChat(items[0].uri, 'SSI credential details' , items[0].timestamp); m.system = true; messages.push(m); } if (items[0].tags.indexOf('ssi-connection') > -1) { let content = ''; let m; chatInputClass = this.noChatInputToolbar; content = 'Role: ' + items[0].ssiConnection.role; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); content = 'State: ' + items[0].ssiConnection.state; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); content = 'Multiple use: ' + items[0].ssiConnection.multiUseInvitation; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); if (items[0].ssiConnection.mediatorId) { content = 'Mediator: ' + items[0].ssiConnection.mediatorId; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); } content = 'Id: ' + items[0].ssiConnection.id; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); content = 'Did: ' + items[0].ssiConnection.did; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); content = 'From: ' + items[0].ssiConnection.theirLabel; m = this.ssi2GiftedChat(items[0].uri, content.trim(), items[0].timestamp); messages.push(m); m = this.ssi2GiftedChat(items[0].uri, 'SSI connection details' , items[0].timestamp); m.system = true; messages.push(m); } } let columns = 1; if (this.state.isTablet) { columns = this.props.orientation === 'landscape' ? 3 : 2; } else { columns = this.props.orientation === 'landscape' ? 2 : 1; } const chatContainer = this.props.orientation === 'landscape' ? styles.chatLandscapeContainer : styles.chatPortraitContainer; const container = this.props.orientation === 'landscape' ? styles.landscapeContainer : styles.portraitContainer; const contactsContainer = this.props.orientation === 'landscape' ? styles.contactsLandscapeContainer : styles.contactsPortraitContainer; const borderClass = (messages.length > 0 && !this.state.chat) ? styles.chatBorder : null; let filteredMessages = []; messages.forEach((m) => { if (!m.image && m.url && !m.local_url) { //return; } if (m.url || m.local_url || m.image) { //console.log('----'); //console.log('Render message local_url', m.failed); } filteredMessages.push(m); //console.log(m); }); messages = filteredMessages; let pinned_messages = [] if (this.state.pinned) { messages.forEach((m) => { if (m.pinned) { pinned_messages.push(m); } }); messages = pinned_messages; if (pinned_messages.length === 0) { let msg = { _id: uuid.v4(), text: 'No pinned messages found. Touch individual messages to pin them.', system: true } pinned_messages.push(msg); } } let showLoadEarlier = (this.state.myContacts && this.state.selectedContact && this.state.selectedContact.uri in this.state.myContacts && this.state.myContacts[this.state.selectedContact.uri].totalMessages && this.state.myContacts[this.state.selectedContact.uri].totalMessages > messages.length) ? true: false; return ( {items.length === 1 ? (this.renderItem(items[0])) : item.id} key={this.props.orientation} loadEarlier /> } {this.showChat && !this.state.inviteContacts? : (items.length === 1) ? { return null }} renderBubble={this.renderMessageBubble} renderMessageVideo={this.renderMessageVideo} renderMessageText={this.renderMessageText} onSend={this.onSendMessage} renderActions={this.renderCustomActions} lockStyle={styles.lock} onLongPress={this.onLongMessagePress} shouldUpdateMessage={this.shouldUpdateMessage} onPress={this.onLongMessagePress} scrollToBottom={this.state.scrollToBottom} inverted={true} timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} infiniteScroll loadEarlier={showLoadEarlier} onLoadEarlier={this.loadEarlierMessages} /> : null } ); } } ContactsListBox.propTypes = { account : PropTypes.object, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, targetUri : PropTypes.string, selectedContact : PropTypes.object, contacts : PropTypes.array, chat : PropTypes.bool, orientation : PropTypes.string, setTargetUri : PropTypes.func, isTablet : PropTypes.bool, isLandscape : PropTypes.bool, refreshHistory : PropTypes.bool, saveHistory : PropTypes.func, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, setFavoriteUri : PropTypes.func, saveConference : PropTypes.func, myInvitedParties: PropTypes.object, setBlockedUri : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, filter : PropTypes.string, periodFilter : PropTypes.string, defaultDomain : PropTypes.string, saveContact : PropTypes.func, myContacts : PropTypes.object, messages : PropTypes.object, getMessages : PropTypes.func, confirmRead : PropTypes.func, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, deleteMessage : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, deleteMessages : PropTypes.func, sendPublicKey : PropTypes.func, inviteContacts : PropTypes.bool, shareToContacts : PropTypes.bool, selectedContacts: PropTypes.array, toggleBlocked : PropTypes.func, togglePinned : PropTypes.func, loadEarlierMessages: PropTypes.func, newContactFunc : PropTypes.func, messageZoomFactor: PropTypes.string, isTyping : PropTypes.bool, fontScale : PropTypes.number, call : PropTypes.object, ssiCredentials: PropTypes.array, ssiConnections: PropTypes.array }; export default ContactsListBox;