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;