diff --git a/app/app.js b/app/app.js index ae725ca..888d90c 100644 --- a/app/app.js +++ b/app/app.js @@ -1,9461 +1,9465 @@ // copyright AG Projects 2020-2021 import React, { Component, Fragment } from 'react'; import { Alert, View, SafeAreaView, ImageBackground, AppState, Linking, Platform, StyleSheet, Vibration, PermissionsAndroid} from 'react-native'; import { DeviceEventEmitter, BackHandler } from 'react-native'; import { Provider as PaperProvider, DefaultTheme } from 'react-native-paper'; import { registerGlobals } from 'react-native-webrtc'; import { Router, Route, Link, Switch } from 'react-router-native'; import history from './history'; import Logger from "../Logger"; import autoBind from 'auto-bind'; import { firebase } from '@react-native-firebase/messaging'; import VoipPushNotification from 'react-native-voip-push-notification'; import uuid from 'react-native-uuid'; import { getUniqueId, getBundleId, isTablet, getPhoneNumber} from 'react-native-device-info'; import RNDrawOverlay from 'react-native-draw-overlay'; import PushNotificationIOS from "@react-native-community/push-notification-ios"; import PushNotification , {Importance} from "react-native-push-notification"; import Contacts from 'react-native-contacts'; import BackgroundTimer from 'react-native-background-timer'; import DeepLinking from 'react-native-deep-linking'; import base64 from 'react-native-base64'; import SoundPlayer from 'react-native-sound-player'; import RNSimpleCrypto from "react-native-simple-crypto"; import OpenPGP from "react-native-fast-openpgp"; import ShortcutBadge from 'react-native-shortcut-badge'; import { getAppstoreAppMetadata } from "react-native-appstore-version-checker"; //import ReceiveSharingIntent from 'react-native-receive-sharing-intent'; import {Keyboard} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import RNBackgroundDownloader from 'react-native-background-downloader'; import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions'; import cloneDeep from 'lodash/cloneDeep'; registerGlobals(); import * as sylkrtc from 'react-native-sylkrtc'; import InCallManager from 'react-native-incall-manager'; import RNCallKeep, { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep'; import RegisterBox from './components/RegisterBox'; import ReadyBox from './components/ReadyBox'; import Call from './components/Call'; import Conference from './components/Conference'; import FooterBox from './components/FooterBox'; import StatusBox from './components/StatusBox'; import ImportPrivateKeyModal from './components/ImportPrivateKeyModal'; import IncomingCallModal from './components/IncomingCallModal'; import LogsModal from './components/LogsModal'; import NotificationCenter from './components/NotificationCenter'; import LoadingScreen from './components/LoadingScreen'; import NavigationBar from './components/NavigationBar'; import Preview from './components/Preview'; import CallManager from './CallManager'; import SQLite from 'react-native-sqlite-storage'; //SQLite.DEBUG(true); SQLite.enablePromise(true); import xtype from 'xtypejs'; import xss from 'xss'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import momenttz from 'moment-timezone'; import utils from './utils'; import config from './config'; import storage from './storage'; import { Agent, AutoAcceptCredential, AutoAcceptProof, ConnectionEventTypes, ConnectionInvitationMessage, ConnectionRecord, ConnectionStateChangedEvent, ConsoleLogger, CredentialEventTypes, CredentialRecord, CredentialState, CredentialStateChangedEvent, HttpOutboundTransport, WsOutboundTransport, InitConfig, LogLevel, } from '@aries-framework/core'; import {agentDependencies} from '@aries-framework/react-native'; var randomString = require('random-string'); const RNFS = require('react-native-fs'); const logfile = RNFS.DocumentDirectoryPath + '/logs.txt'; import styles from './assets/styles/blink/root.scss'; const backgroundImage = require('./assets/images/dark_linen.png'); const logger = new Logger("App"); function checkIosPermissions() { return new Promise(resolve => PushNotificationIOS.checkPermissions(resolve)); } const KeyOptions = { cipher: "aes256", compression: "zlib", hash: "sha512", RSABits: 4096, compressionLevel: 5 } const incomingCallLabel = 'Incoming call...'; const theme = { ...DefaultTheme, dark: true, roundness: 2, colors: { ...DefaultTheme.colors, primary: '#337ab7', // accent: '#f1c40f', }, }; const URL_SCHEMES = [ 'sylk://', ]; const ONE_SECOND_IN_MS = 1000; const VIBRATION_PATTERN = [ 1 * ONE_SECOND_IN_MS, 1 * ONE_SECOND_IN_MS, 4 * ONE_SECOND_IN_MS ]; let bundleId = `${getBundleId()}`; const deviceId = getUniqueId(); const version = '1.0.0'; const MAX_LOG_LINES = 300; if (Platform.OS == 'ios') { bundleId = `${bundleId}.${__DEV__ ? 'dev' : 'prod'}`; //bundleId = 'com.agprojects.sylk-ios.dev'; } const mainStyle = StyleSheet.create({ MainContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', margin: 0 } }); function _parseSQLDate(key, value) { return new Date(value); } (function() { if ( typeof Object.id == "undefined" ) { var id = 0; Object.id = function(o) { if ( o && typeof o.__uniqueid == "undefined" ) { Object.defineProperty(o, "__uniqueid", { value: ++id, enumerable: false, // This could go either way, depending on your // interpretation of what an "id" is writable: false }); } return o ? o.__uniqueid : null; }; } })(); class Sylk extends Component { constructor() { super(); autoBind(this) this._loaded = false; let isFocus = Platform.OS === 'ios'; this.startTimestamp = new Date(); this._initialState = { appState: null, autoLogin: true, inFocus: isFocus, accountId: '', password: '', displayName: '', fontScale: 1, email: '', organization: '', account: null, keyStatus: {}, lastSyncId: null, accountVerified: false, registrationState: null, registrationKeepalive: false, incomingCall: null, currentCall: null, connection: null, showScreenSharingModal: false, status: null, targetUri: '', missedTargetUri: '', loading: null, syncConversations: false, localMedia: null, generatedVideoTrack: false, contacts: [], devices: {}, speakerPhoneEnabled: null, orientation : 'portrait', keyboardVisible: false, Height_Layout : '', Width_Layout : '', outgoingCallUUID: null, incomingCallUUID: null, incomingContact: null, keyboardHeight: 0, hardware: '', phoneNumber: '', isTablet: isTablet(), refreshHistory: false, refreshFavorites: false, myPhoneNumber: null, favoriteUris: [], blockedUris: [], missedCalls: [], initialUrl: null, reconnectingCall: false, muted: false, participantsToInvite: [], myInvitedParties: {}, myContacts: {}, defaultDomain: config.defaultDomain, fileSharingUrl: config.fileSharingUrl, declineReason: null, showLogsModal: false, logs: '', proximityEnabled: true, messages: {}, selectedContact: null, callsState: {}, keys: null, showImportPrivateKeyModal: false, privateKey: null, privateKeyImportStatus: '', privateKeyImportSuccess: false, inviteContacts: false, shareToContacts: false, shareContent: [], selectedContacts: [], pinned: false, callContact: null, messageLimit: 100, messageZoomFactor: 1, messageStart: 0, contactsLoaded: false, replicateContacts: {}, updateContactUris: {}, blockedContacts: {}, decryptingMessages: {}, purgeMessages: [], showCallMeMaybeModal: false, enrollment: false, contacts: [], isTyping: false, avatarPhotos: {}, avatarEmails: {}, showConferenceModal: false, keyDifferentOnServer: false, keyExistsOnServer: false, serverPublicKey: null, generatingKey: false, appStoreVersion: null, firstSyncDone: false, keysNotFound: false, showLogo: true, historyFilter: null, showExportPrivateKeyModal: false, showQRCodeScanner: false, navigationItems: {today: false, yesterday: false, conference: false}, ssiRequired: false, ssiAgent: null, ssiRoles: [], myuuid: null }; utils.timestampedLog('Init app'); this.timeoutIncomingTimer = null; this.pendingNewSQLMessages = []; this.newSyncMessagesCount = 0; this.syncStartTimestamp = null; this.syncRequested = false; this.mustSendPublicKey = false; this.conferenceEndedTimer = null; this.syncTimer = null; this.lastSyncedMessageId = null; this.outgoingMedia = null; this.participantsToInvite = []; this.tokenSent = false; this.mustLogout = false; this.currentRoute = null; this.pushtoken = null; this.pushkittoken = null; this.intercomDtmfTone = null; this.registrationFailureTimer = null; this.startedByPush = false; this.heartbeats = 0; this.sql_contacts_keys = []; this._onFinishedPlayingSubscription = null this._onFinishedLoadingSubscription = null this._onFinishedLoadingFileSubscription = null this._onFinishedLoadingURLSubscription = null this.cancelRingtoneTimer = null; this.sync_pending_items = []; this.signup = {}; this.last_signup = null; this.keyboardDidShowListener = null; this.state = Object.assign({}, this._initialState); this.myParticipants = {}; this.mySyncJournal = {}; this._historyConferenceParticipants = new Map(); // for saving to local history this._terminatedCalls = new Map(); this.__notificationCenter = null; this.redirectTo = null; this.prevPath = null; this.shouldUseHashRouting = false; this.goToReadyTimer = null; this.incoming_sound_ts = null; this.outgoing_sound_ts = null; this.initialChatContact = null; this.mustPlayIncomingSoundAfterSync = false; this.ssiAgent = null; storage.initialize(); this.callKeeper = new CallManager(RNCallKeep, this.showAlertPanel, this.acceptCall, this.rejectCall, this.hangupCall, this.timeoutCall, this.callKeepStartConference, this.startCallFromCallKeeper, this.toggleMute, this.getConnection, this.addHistoryEntry, this.changeRoute, this.respawnConnection, this.isUnmounted ); if (InCallManager.recordPermission !== 'granted') { /* console.log('InCallManager request record permission'); InCallManager.requestRecordPermission() .then((requestedRecordPermissionResult) => { console.log("InCallManager.requestRecordPermission() requestedRecordPermissionResult: ", requestedRecordPermissionResult); }) .catch((err) => { console.log("InCallManager.requestRecordPermission() catch: ", err); }); */ } else { console.log('InCallManager recordPermission', InCallManager.recordPermission); } // Load camera/mic preferences storage.get('devices').then((devices) => { if (devices) { this.setState({devices: devices}); } }); storage.get('account').then((account) => { if (account) { this.setState({accountVerified: account.verified}); } }); storage.get('keys').then((keys) => { if (keys) { const public_key = keys.public.replace(/\r/g,''); const private_key = keys.private.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; this.setState({keys: keys}); console.log("Loaded PGP public key"); } }).catch((err) => { console.log("PGP keys loading error:", err); }); storage.get('ssi').then((ssi) => { if (ssi) { - //console.log("Loaded SSI settings", ssi); + console.log("Loaded SSI settings", ssi); this.setState({ssiRequired: ssi.required}); } else { console.log("Init SSI settings", ssi); storage.set('ssi', {required: false}); this.setState({ssiRequired: false}); } }).catch((err) => { console.log("SSI settings loading error:", err); }); storage.get('myParticipants').then((myParticipants) => { if (myParticipants) { this.myParticipants = myParticipants; //console.log('My participants', this.myParticipants); } }); storage.get('signup').then((signup) => { if (signup) { this.signup = signup; } }); storage.get('last_signup').then((last_signup) => { if (last_signup) { this.last_signup = last_signup; } }); storage.get('mySyncJournal').then((mySyncJournal) => { if (mySyncJournal) { this.mySyncJournal = mySyncJournal; } }); storage.get('lastSyncedMessageId').then((lastSyncedMessageId) => { if (lastSyncedMessageId) { this.lastSyncedMessageId = lastSyncedMessageId; } }); storage.get('proximityEnabled').then((proximityEnabled) => { this.setState({proximityEnabled: proximityEnabled}); }); if (this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } this.loadPeople(); for (let scheme of URL_SCHEMES) { DeepLinking.addScheme(scheme); } this.sqlTableVersions = {'messages': 8, 'contacts': 7, 'keys': 3} this.updateTableQueries = {'messages': {1: [], 2: [{query: 'delete from messages', params: []}], 3: [{query: 'alter table messages add column unix_timestamp INTEGER default 0', params: []}], 4: [{query: 'alter table messages add column account TEXT', params: []}], 5: [{query: 'update messages set account = from_uri where direction = ?' , params: ['outgoing']}, {query: 'update messages set account = to_uri where direction = ?', params: ['incoming']}], 6: [{query: 'alter table messages add column sender TEXT' , params: []}], 7: [{query: 'alter table messages add column image TEXT' , params: []}, {query: 'alter table messages add column local_url TEXT' , params: []}], 8: [{query: 'alter table messages add column metadata TEXT' , params: []}] }, 'contacts': {2: [{query: 'alter table contacts add column participants TEXT', params: []}], 3: [{query: 'alter table contacts add column direction TEXT', params: []}, {query: 'alter table contacts add column last_call_media TEXT', params: []}, {query: 'alter table contacts add column last_call_duration INTEGER default 0', params: []}, {query: 'alter table contacts add column last_call_id TEXT', params: []}, {query: 'alter table contacts add column conference INTEGER default 0', params: []}], 4: [{query: 'CREATE TABLE contacts2 as SELECT uri, account, name, organization, tags, participants, public_key, timestamp, direction, last_message, last_message_id, unread_messages, last_call_media, last_call_duration, last_call_id, conference from contacts', params: []}, {query: 'CREATE TABLE contacts3 (uri TEXT, account TEXT, name TEXT, organization TEXT, tags TEXT, participants TEXT, public_key TEXT, timestamp INTEGER, direction TEXT, last_message TEXT, last_message_id TEXT, unread_messages TEXT, last_call_media TEXT, last_call_duration INTEGER default 0, last_call_id TEXT, conference INTEGER default 0, PRIMARY KEY (account, uri))', params: []}, {query: 'drop table contacts', params: []}, {query: 'drop table contacts2', params: []}, {query: 'ALTER TABLE contacts3 RENAME TO contacts', params: []} ], 5: [{query: 'alter table contacts add column email TEXT', params: []}], 6: [{query: 'alter table contacts add column photo BLOB', params: []}], 7: [{query: 'alter table contacts add column email TEXT', params: []}] }, 'keys': {2: [{query: 'alter table keys add column last_sync_id TEXT', params: []}], 3: [{query: 'alter table keys add column my_uuid TEXT', params: []}] } }; this.db = null; this.initSQL(); } async requestStoragePermission() { if (Platform.OS !== 'android') { return; } console.log('Request storage permission'); try { const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE; await PermissionsAndroid.request(permission); Promise.resolve(); } catch (error) { Promise.reject(error); } } async requestCameraPermission() { console.log('Request camera permission'); if (Platform.OS === 'ios') { check(PERMISSIONS.IOS.CAMERA).then((result) => { switch (result) { case RESULTS.UNAVAILABLE: console.log('Camera feature is not available (on this device / in this context)'); break; case RESULTS.DENIED: console.log('Camera permission has not been requested / is denied but requestable'); this._notificationCenter.postSystemNotification("Access to camera is denied. Go to Settings -> Sylk to enable access."); break; case RESULTS.LIMITED: console.log('Camera permission is limited: some actions are possible'); break; case RESULTS.GRANTED: console.log('Camera permission is granted'); break; case RESULTS.BLOCKED: this._notificationCenter.postSystemNotification("Access to camera is denied. Go to Settings -> Sylk to enable access."); console.log('Camera permission is denied and not requestable anymore'); break; } }).catch((error) => { }); return true; } if (Platform.OS === 'android') { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.CAMERA, { title: "Sylk camera permission", message: "Sylk needs access to your camera " + "for video calls", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can use the camera"); return true; } else { console.log("Camera permission denied"); return false; } } catch (err) { console.warn(err); return false; } } } async requestMicPermission() { console.log('Request mic permission'); if (Platform.OS === 'ios') { check(PERMISSIONS.IOS.MICROPHONE).then((result) => { switch (result) { case RESULTS.UNAVAILABLE: console.log('Mic feature is not available (on this device / in this context)'); break; case RESULTS.DENIED: console.log('Mic permission has not been requested / is denied but requestable'); this._notificationCenter.postSystemNotification("Access to microphone is denied. Go to Settings -> Sylk to enable access."); break; case RESULTS.LIMITED: console.log('Mic permission is limited: some actions are possible'); break; case RESULTS.GRANTED: console.log('Mic permission is granted'); break; case RESULTS.BLOCKED: this._notificationCenter.postSystemNotification("Access to microphone is denied. Go to Settings -> Sylk to enable access."); console.log('Mic permission is denied and not requestable anymore'); break; } }).catch((error) => { }); return true; } if (Platform.OS === 'android') { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: "Sylk microphone permission", message: "Sylk needs access to your microphone " + "for audio calls.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can now use the microphone"); return true; } else { console.log("Microphone permission denied"); return false; } } catch (err) { console.warn(err); return false; } } }; useExistingKeys() { var uri = uuid.v4() + '@' + this.state.defaultDomain; console.log('Send public key to', uri); this.sendPublicKey(uri); this.setState({keyDifferentOnServer: false}); } async saveMyKey(keys) { let keyStatus = this.state.keyStatus; keyStatus['existsLocal'] = true; this.setState({keys: {private: keys.private, public: keys.public, showImportPrivateKeyModal: false, keyStatus: keyStatus }}); let myContacts = this.state.myContacts; if (this.state.account) { this.requestSyncConversations(); this.useExistingKeys(); let accountId = this.state.account.id; if (accountId in myContacts) { } else { myContacts[accountId] = this.newContact(accountId); } myContacts[accountId].publicKey = keys.public; this.saveSylkContact(accountId, myContacts[accountId], 'PGP key generated'); } else { console.log('Send 1st public key later'); this.mustSendPublicKey = true; } let current_datetime = new Date(); const unixTime = Math.floor(current_datetime / 1000); const my_uuid = uuid.v4(); let params = [this.state.accountId, keys.private, keys.public, unixTime, my_uuid]; await this.ExecuteQuery("INSERT INTO keys (account, private_key, public_key, timestamp, my_uuid) VALUES (?, ?, ?, ?, ?)", params).then((result) => { console.log('SQL inserted private key'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') > -1) { this.updateKeySql(keys); } else { console.log('Save keys SQL error:', error); } }); } async saveLastSyncId(id, force=false) { if (!force) { if (!this.state.keys || !this.state.keys.private) { console.log('Skip saving last sync id until we have a private key'); return } if (!this.state.firstSyncDone) { console.log('Skip saving last sync id until first sync is done'); return } } let params = [id, this.state.accountId]; await this.ExecuteQuery("update keys set last_sync_id = ? where account = ?", params).then((result) => { console.log('SQL saved last sync id', id); this.setState({lastSyncId: id}); }).catch((error) => { console.log('Save last sync id SQL error:', error); }); } async updateKeySql(keys) { let current_datetime = new Date(); const unixTime = Math.floor(current_datetime / 1000); let params = [keys.private, keys.public, unixTime, this.state.accountId]; await this.ExecuteQuery("update keys set private_key = ?, public_key = ?, timestamp = ? where account = ?", params).then((result) => { console.log('SQL updated private key'); }).catch((error) => { console.log('SQL update keys error:', error); }); } async updateMyUUID() { const my_uuid = uuid.v4(); let params = [my_uuid, this.state.accountId]; await this.ExecuteQuery("update keys set my_uuid = ? where account = ?", params).then((result) => { - console.log('SQL updated my uuid to', my_uuid); + utils.timestampedLog('My device UUID was updated', my_uuid); this.setState({myuuid: my_uuid}); }).catch((error) => { console.log('SQL update uuid error:', error); }); } loadMyKeys() { console.log('Loading PGP keys...'); let keys = {}; let lastSyncId; let keyStatus = this.state.keyStatus; this.ExecuteQuery("SELECT * FROM keys where account = ?",[this.state.accountId]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); //console.log('SQL has keys'); keys.public = item.public_key; if (item.public_key) { //keyStatus['serverPublicKey'] === item.public_key) { keyStatus['existsLocal'] = true; this.setState({showImportPrivateKeyModal: false}); } else { keyStatus['existsLocal'] = false; } let my_uuid = item.my_uuid; if (!my_uuid) { this.updateMyUUID(); } else { - console.log('My device UUID', my_uuid); + utils.timestampedLog('My device UUID', my_uuid); this.setState({myuuid: my_uuid}); } keys.private = item.private_key; console.log('Loaded PGP private key for account', this.state.accountId); if (!item.last_sync_id && this.lastSyncedMessageId) { this.setState({keys: keys}); this.saveLastSyncId(this.lastSyncedMessageId); console.log('Migrated last sync id to SQL database'); storage.remove('lastSyncedMessageId'); lastSyncId = this.lastSyncedMessageId; } else { if (item.last_sync_id) { console.log('Loaded last sync id', item.last_sync_id); } this.setState({keys: keys, lastSyncId: item.last_sync_id}); lastSyncId = item.last_sync_id; } if (this.state.registrationState === 'registered') { this.requestSyncConversations(lastSyncId); } } else { //console.log('SQL has no keys'); keyStatus['existsLocal'] = false; if (this.state.account) { this.generateKeysIfNecessary(this.state.account); } else { console.log('Wait for account become active...'); } } this.setState({contactsLoaded: true, keyStatus: keyStatus}); this.getDownloadTasks(); }); } async getDownloadTasks() { let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); //console.log('Download lost tasks', lostTasks); for (let task of lostTasks) { console.log(`Download task ${task.id} was found:`, task.url); task.progress((percent) => { console.log(task.url, `Downloaded: ${percent * 100}%`); }).done(() => { this.updateFileDownload(id, task.url, task.destination); }).error((error) => { console.log(task.url, 'download error:', error); }); } } async generateKeys() { const Options = { comment: 'Sylk key', email: this.state.accountId, name: this.state.displayName || this.state.accountId, keyOptions: KeyOptions } console.log('Generating key pair with options', Options); this.setState({loading: 'Generating private key...', generatingKey: true}); await OpenPGP.generate(Options).then((keys) => { const public_key = keys.publicKey.replace(/\r/g, '').trim(); const private_key = keys.privateKey.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; console.log("PGP keypair generated"); this.setState({loading: null, generatingKey: false}); this.setState({showImportPrivateKeyModal: false}); this.saveMyKey(keys); this.showCallMeModal(); }).catch((error) => { console.log("PGP keys generation error:", error); }); } resetStorage() { return; console.log('Reset storage'); this.ExecuteQuery('delete from contacts'); this.ExecuteQuery('delete from messages'); this.saveLastSyncId(null); } loadSylkContacts() { console.log('Loading contacts...') let myContacts = {}; let blockedUris = []; let favoriteUris = []; let missedCalls = []; let myInvitedParties = {}; let localTime; let email; let contact; let timestamp; this.loadAddressBook(); if (this.state.accountId in this.signup) { email = this.signup[this.state.accountId]; this.setState({email: email}); } if (!this.last_signup) { storage.set('last_signup', this.state.accountId); if (this.state.accountId in this.signup) { } else { this.signup[this.state.accountId] = ''; storage.set('signup', this.signup); } } this.setState({defaultDomain: this.state.accountId.split('@')[1]}); this.ExecuteQuery("SELECT * FROM contacts where account = ? order by timestamp desc",[this.state.accountId]).then((results) => { let rows = results.rows; let idx; let formatted_date; let updated; //console.log(rows.length, 'SQL rows'); if (rows.length > 0) { for (let i = 0; i < rows.length; i++) { var item = rows.item(i); updated = null; if (!item.uri) { continue; } contact = this.newContact(item.uri, item.name, {src: 'init'}); if (!contact) { continue; } this.sql_contacts_keys.push(item.uri); timestamp = new Date(item.timestamp * 1000); if (timestamp > new Date()) { timestamp = new Date(); updated = 'timestamp'; } myContacts[item.uri] = contact; myContacts[item.uri].organization = item.organization; myContacts[item.uri].email = item.email; myContacts[item.uri].photo = item.photo; myContacts[item.uri].publicKey = item.public_key; myContacts[item.uri].direction = item.direction; myContacts[item.uri].tags = item.tags ? item.tags.split(',') : []; myContacts[item.uri].participants = item.participants ? item.participants.split(',') : []; myContacts[item.uri].unread = item.unread_messages ? item.unread_messages.split(',') : []; myContacts[item.uri].lastMessageId = item.last_message_id === '' ? null : item.last_message_id; myContacts[item.uri].lastMessage = item.last_message === '' ? null : item.last_message; myContacts[item.uri].timestamp = timestamp; myContacts[item.uri].lastCallId = item.last_call_id; myContacts[item.uri].lastCallMedia = item.last_call_media ? item.last_call_media.split(',') : []; myContacts[item.uri].lastCallDuration = item.last_call_duration; let ab_contacts = this.lookupContacts(item.uri); if (ab_contacts.length > 0) { if (!myContacts[item.uri].name || myContacts[item.uri].name === '') { console.log('Update display name', myContacts[item.uri].name, 'of', item.uri, 'to', ab_contacts[0].name); myContacts[item.uri].name = ab_contacts[0].name; updated = 'name'; } myContacts[item.uri].label = ab_contacts[0].label; if (myContacts[item.uri].tags.indexOf('contact') === -1) { myContacts[item.uri].tags.push('contact'); updated = 'tags'; } } if (!myContacts[item.uri].photo) { var name_idx = myContacts[item.uri].name.trim().toLowerCase(); if (name_idx in this.state.avatarPhotos) { myContacts[item.uri].photo = this.state.avatarPhotos[name_idx]; updated = 'photo'; } } if (!myContacts[item.uri].email) { var name_idx = myContacts[item.uri].name.trim().toLowerCase(); if (name_idx in this.state.avatarEmails) { myContacts[item.uri].email = this.state.avatarEmails[name_idx]; updated = 'email'; } } if (myContacts[item.uri].tags.indexOf('missed') > -1) { missedCalls.push(item.last_call_id); if (myContacts[item.uri].unread.indexOf(item.last_call_id) === -1) { myContacts[item.uri].unread.push(item.last_call_id); } } else { idx = myContacts[item.uri].unread.indexOf(item.last_call_id); if (idx > -1) { myContacts[item.uri].unread.splice(idx, 1); } } if (item.uri === this.state.accountId) { this.setState({displayName: item.name, organization: item.organization}); if (email && !item.email) { item.email = email; } else { this.setState({email: item.email}); } } formatted_date = myContacts[item.uri].timestamp.getFullYear() + "-" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getMonth() + 1) + "-" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getDate()) + " " + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getHours()) + ":" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getMinutes()) + ":" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getSeconds()); //console.log('Loaded contact', formatted_date, item.uri, item.name); if(item.participants) { myInvitedParties[item.uri.split('@')[0]] = myContacts[item.uri].participants; } if (myContacts[item.uri].tags.indexOf('blocked') > -1) { blockedUris.push(item.uri); } if (myContacts[item.uri].tags.indexOf('favorite') > -1) { favoriteUris.push(item.uri); } if (updated) { this.saveSylkContact(item.uri, myContacts[item.uri], 'update contact at init because of ' + updated); } //console.log('Load contact', item.uri, '-', item.name); } storage.get('cachedHistory').then((history) => { if (history) { //this.cachedHistory = history; history.forEach((item) => { //console.log(item); if (item.remoteParty in myContacts) { } else { myContacts[item.remoteParty] = this.newContact(item.remoteParty); } if (item.timezone && item.timezone !== undefined) { localTime = momenttz.tz(item.startTime, item.timezone).toDate(); if (localTime > myContacts[item.remoteParty].timestamp) { myContacts[item.remoteParty].timestamp = localTime; } } myContacts[item.remoteParty].name = item.displayName; myContacts[item.remoteParty].direction = item.direction === 'received' ? 'incoming' : 'outgoing'; myContacts[item.remoteParty].lastCallId = item.sessionId; myContacts[item.remoteParty].lastCallDuration = item.duration; myContacts[item.remoteParty].lastCallMedia = item.media; myContacts[item.remoteParty].conference = item.conference; myContacts[item.remoteParty].tags.push('history'); this.saveSylkContact(item.remoteParty, this.state.myContacts[item.remoteParty], 'init'); }); console.log('Migrated', history.length, 'server history entries'); storage.remove('cachedHistory'); } }); storage.get('history').then((history) => { if (history) { console.log('Loaded', history.length, 'local history entries'); history.forEach((item) => { if (item.remoteParty in myContacts) { } else { myContacts[item.remoteParty] = this.newContact(item.remoteParty); } if (item.timezone && item.timezone !== undefined) { localTime = momenttz.tz(item.startTime, item.timezone).toDate(); if (localTime > myContacts[item.remoteParty].timestamp) { myContacts[item.remoteParty].timestamp = localTime; } } myContacts[item.remoteParty].name = item.displayName; myContacts[item.remoteParty].direction = item.direction === 'received' ? 'incoming' : 'outgoing'; myContacts[item.remoteParty].lastCallId = item.sessionId; myContacts[item.remoteParty].lastCallDuration = item.duration; myContacts[item.remoteParty].lastCallMedia = item.media; myContacts[item.remoteParty].conference = item.conference; myContacts[item.remoteParty].tags.push('history'); this.saveSylkContact(item.remoteParty, this.state.myContacts[item.remoteParty], 'init'); }); console.log('Migrated', history.length, 'local history entries'); storage.remove('history'); } }); this.updateTotalUread(myContacts); console.log('Loaded', rows.length, 'contacts for account', this.state.accountId); this.setState({myContacts: myContacts, missedCalls: missedCalls, favoriteUris: favoriteUris, myInvitedParties: myInvitedParties, blockedUris: blockedUris}); } else { if (Object.keys(this.state.myContacts).length > 0) { Object.keys(this.state.myContacts).forEach((key) => { this.saveSylkContact(key, this.state.myContacts[key], 'init'); }); storage.set('contactStorage', 'sql'); storage.remove('myContacts'); } } this.refreshNavigationItems(); setTimeout(() => { if (this.initialChatContact) { console.log('Starting chat with', this.initialChatContact); if (this.initialChatContact in this.state.myContacts) { this.selectContact(this.state.myContacts[this.initialChatContact]); } else { this.initialChatContact = null; } } }, 100); setTimeout(() => { //this.getMessages(); }, 500); this.loadMyKeys(); }); } addTestContacts() { let myContacts = this.state.myContacts; let test_numbers = [ {uri: '4444@sylk.link', name: 'Test microphone'}, {uri: '3333@sylk.link', name: 'Test video'} ]; test_numbers.forEach((item) => { if (Object.keys(myContacts).indexOf(item.uri) === -1) { myContacts[item.uri] = this.newContact(item.uri, item.name, {src: 'init'}); myContacts[item.uri].tags.push('test'); this.saveSylkContact(item.uri, myContacts[item.uri], 'init uri'); } else { if (myContacts[item.uri].tags.indexOf('test') === -1) { myContacts[item.uri].tags.push('test'); this.saveSylkContact(item.uri, myContacts[item.uri], 'init tags'); } if (!myContacts[item.uri].name) { myContacts[item.uri].name = item.name; this.saveSylkContact(item.uri, myContacts[item.uri], 'init name'); } } }); } loadPeople() { let myContacts = {}; let blockedUris = []; let favoriteUris = []; let displayName = null; storage.get('contactStorage').then((contactStorage) => { if (contactStorage !== 'sql') { storage.get('myContacts').then((myContacts) => { let myContactsObjects = {}; if (myContacts) { Object.keys(myContacts).forEach((key) => { if (!Array.isArray(myContacts[key]['unread'])) { myContacts[key]['unread'] = []; } if(typeof(myContacts[key]) == 'string') { console.log('Convert display name object'); myContactsObjects[key] = {'name': myContacts[key]} } else { myContactsObjects[key] = myContacts[key]; } }); myContacts = myContactsObjects; } else { myContacts = {}; } this.setState({myContacts: myContacts}); storage.get('favoriteUris').then((favoriteUris) => { favoriteUris = favoriteUris.filter(item => item !== null); //console.log('My favorites:', favoriteUris); this.setState({favoriteUris: favoriteUris}); storage.remove('favoriteUris'); }).catch((error) => { //console.log('get favoriteUris error:', error); let uris = Object.keys(myContacts); uris.forEach((uri) => { if (myContacts[uri].favorite) { favoriteUris.push(uri); } }); this.setState({favoriteUris: favoriteUris}); }); storage.get('blockedUris').then((blockedUris) => { blockedUris = blockedUris.filter(item => item !== null); this.setState({blockedUris: blockedUris}); storage.remove('blockedUris'); }).catch((error) => { //console.log('get blockedUris error:', error); let uris = Object.keys(myContacts); uris.forEach((uri) => { if (myContacts[uri].blocked) { blockedUris.push(uri); } }); this.setState({blockedUris: blockedUris}); }); }).catch((error) => { console.log('get myContacts error:', error); }); } }); } async initSQL() { const database_name = "sylk.db"; const database_version = "1.0"; const database_displayname = "Sylk Database"; const database_size = 200000; await SQLite.openDatabase(database_name, database_version, database_displayname, database_size).then((DB) => { this.db = DB; console.log('SQL database', database_name, 'opened'); this.resetStorage(); //this.dropTables(); this.createTables(); }).catch((error) => { console.log('SQL database error:', error); }); } dropTables() { console.log('Drop SQL tables...') this.ExecuteQuery("DROP TABLE if exists 'chat_uris';"); this.ExecuteQuery("DROP TABLE if exists 'recipients';"); this.ExecuteQuery("DROP TABLE 'messages';"); this.ExecuteQuery("DROP TABLE 'versions';"); } createTables() { //console.log('Create SQL tables...') let create_versions_table = "CREATE TABLE IF NOT EXISTS 'versions' ( \ 'id' INTEGER PRIMARY KEY AUTOINCREMENT, \ 'table' TEXT UNIQUE, \ 'version' INTEGER NOT NULL );\ "; this.ExecuteQuery(create_versions_table).then((success) => { //console.log('SQL version table created'); }).catch((error) => { console.log(create_versions_table); console.log('SQL version table creation error:', error); }); let create_table_messages = "CREATE TABLE IF NOT EXISTS 'messages' ( \ 'msg_id' TEXT, \ 'timestamp' TEXT, \ 'account' TEXT, \ 'unix_timestamp' INTEGER default 0, \ 'sender' TEXT, \ 'content' BLOB, \ 'content_type' TEXT, \ 'metadata' TEXT, \ 'from_uri' TEXT, \ 'to_uri' TEXT, \ 'sent' INTEGER, \ 'sent_timestamp' TEXT, \ 'received' INTEGER, \ 'received_timestamp' TEXT, \ 'expire_interval' INTEGER, \ 'deleted' INTEGER, \ 'pinned' INTEGER, \ 'pending' INTEGER, \ 'system' INTEGER, \ 'url' TEXT, \ 'local_url' TEXT, \ 'image' TEXT, \ 'encrypted' INTEGER default 0, \ 'direction' TEXT, \ PRIMARY KEY (account, msg_id)) \ "; this.ExecuteQuery(create_table_messages).then((success) => { //console.log('SQL messages table OK'); }).catch((error) => { console.log(create_table_messages); console.log('SQL messages table creation error:', error); }); let create_table_contacts = "CREATE TABLE IF NOT EXISTS 'contacts' ( \ 'uri' TEXT, \ 'account' TEXT, \ 'name' TEXT, \ 'organization' TEXT, \ 'tags' TEXT, \ 'photo' BLOB, \ 'email' TEXT, \ 'participants' TEXT, \ 'public_key' TEXT, \ 'timestamp' INTEGER, \ 'direction' TEXT, \ 'last_message' TEXT, \ 'last_message_id' TEXT, \ 'unread_messages' TEXT, \ 'last_call_media' TEXT, \ 'last_call_duration' INTEGER default 0, \ 'last_call_id' TEXT, \ 'conference' INTEGER default 0, \ PRIMARY KEY (account, uri)) \ "; this.ExecuteQuery(create_table_contacts).then((success) => { //console.log('SQL contacts table OK'); }).catch((error) => { console.log(create_table_contacts); console.log('SQL messages table creation error:', error); }); let create_table_keys = "CREATE TABLE IF NOT EXISTS 'keys' ( \ 'account' TEXT PRIMARY KEY, \ 'private_key' TEXT, \ 'checksum' TEXT, \ 'public_key' TEXT, \ 'last_sync_id' TEXT, \ 'timestamp' INTEGER, \ 'my_uuid' TEXT) \ "; this.ExecuteQuery(create_table_keys).then((success) => { //console.log('SQL keys table OK'); }).catch((error) => { console.log(create_table_keys); console.log('SQL keys table creation error:', error); }); this.upgradeSQLTables(); } upgradeSQLTables() { //console.log('Upgrade SQL tables') let query; let update_queries; let update_sub_queries; let version_numbers; /* this.ExecuteQuery("ALTER TABLE 'messages' add column received_timestamp TEXT after received"); this.ExecuteQuery("ALTER TABLE 'messages' add column sent_timestamp TEXT after sent"); */ //query = "update versions set version = \"4\" where \"table\" = 'messages'"; // this.ExecuteQuery(query); query = "SELECT * FROM versions"; let currentVersions = {}; this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; for (let i = 0; i < rows.length; i++) { var item = rows.item(i); currentVersions[item.table] = item.version; //console.log('Table', item.table, 'version', item.version); } for (const [key, value] of Object.entries(this.sqlTableVersions)) { if (currentVersions[key] == null) { query = "INSERT INTO versions ('table', 'version') values ('" + key + "', '" + this.sqlTableVersions[key] + "')"; //console.log(query); this.ExecuteQuery(query); } else { //console.log('Table', key, 'has version', value); if (this.sqlTableVersions[key] > currentVersions[key]) { console.log('Table', key, 'must have version', value, 'and it has', currentVersions[key]); update_queries = this.updateTableQueries[key]; version_numbers = Object.keys(update_queries); version_numbers.sort(function(a, b){return a-b}); version_numbers.forEach((version) => { if (version <= currentVersions[key]) { return; } update_sub_queries = update_queries[version]; update_sub_queries.forEach((query_objects) => { console.log('Run query for table', key, 'version', version, ':', query_objects.query); this.ExecuteQuery(query_objects.query, query_objects.params); }); }); query = "update versions set version = " + this.sqlTableVersions[key] + " where \"table\" = '" + key + "';"; //console.log(query); this.ExecuteQuery(query); } else { //console.log('No upgrade required for table', key); } } } }).catch((error) => { console.log('SQL error:', error); }); } /* * Execute sql queries * * @param sql * @param params * * @returns {resolve} results */ ExecuteQuery = (sql, params = []) => new Promise((resolve, reject) => { //console.log('-- Execute SQL query:', sql, params); //console.log('-- Execute SQL query:', sql); if (!sql) { return; } this.db.transaction((trans) => { trans.executeSql(sql, params, (trans, results) => { resolve(results); }, (error) => { reject(error); }); }); }); async requestReadContactsPermission() { console.log('Request contacts permission...'); try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.READ_CONTACTS, { title: 'Sylk contacts', message: 'Sylk will ask for permission to read your contacts', buttonPositive: "Next" } ) if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can now read your contacts") this.getABContacts(); } else { console.log("Read contacts permission denied") } } catch (err) { console.warn(err) } } async loadAddressBook() { console.log('Load system address book'); Contacts.checkPermission((err, permission) => { //console.log('Current contacts permissions is', permission); if (err) throw err; // Contacts.PERMISSION_AUTHORIZED || Contacts.PERMISSION_UNDEFINED || Contacts.PERMISSION_DENIED if (permission === 'authorized') { this.getABContacts(); return; } if (Platform.OS === 'android') { this.requestReadContactsPermission(); } else { Contacts.requestPermission((err, permission) => { }); } }) } getABContacts() { Contacts.getAll((err, contacts) => { if (err) throw err; // contacts returned in Array let contact_cards = []; let name; let photo; let avatarPhotos = {}; let avatarEmails = {}; let seen_uris = new Map(); var arrayLength = contacts.length; for (var i = 0; i < arrayLength; i++) { photo = null; contact = contacts[i]; if (contact['givenName'] && contact['familyName']) { name = contact['givenName'] + ' ' + contact['familyName']; } else if (contact['givenName']) { name = contact['givenName']; } else if (contact['familyName']) { name = contact['familyName']; } else if (contact['company']) { name = contact['company']; } else { continue; } if (contact.hasThumbnail) { photo = contact.thumbnailPath; } else { photo = null; } //console.log(name); contact['phoneNumbers'].forEach(function (number, index) { let number_stripped = number['number'].replace(/\s|\-|\(|\)/g, ''); if (number_stripped) { if (!seen_uris.has(number_stripped)) { //console.log(' ----> ', number['label'], number_stripped); var contact_card = {id: uuid.v4(), name: name.trim(), uri: number_stripped, type: 'contact', photo: photo, label: number['label'], tags: ['contact']}; if (photo) { var name_idx = name.trim().toLowerCase(); avatarPhotos[name_idx] = photo; } contact_cards.push(contact_card); //console.log('Added AB contact', name, number_stripped); seen_uris.set(number_stripped, true); } } }); contact['emailAddresses'].forEach(function (email, index) { let email_stripped = email['email'].replace(/\s|\(|\)/g, ''); if (!seen_uris.has(email_stripped)) { //console.log(name, email['label'], email_stripped); var contact_card = {id: uuid.v4(), name: name.trim(), uri: email_stripped, type: 'contact', photo: photo, label: email['label'], tags: ['contact'] }; var name_idx = name.trim().toLowerCase(); if (photo) { avatarPhotos[name_idx] = photo; } if (name_idx in avatarEmails) { } else { avatarEmails[name_idx] = email_stripped; } contact_cards.push(contact_card); seen_uris.set(email_stripped, true); } }); } this.setState({contacts: contact_cards, avatarPhotos: avatarPhotos, avatarEmails: avatarEmails}); console.log('Loaded', contact_cards.length, 'addressbook entries'); }) } get _notificationCenter() { // getter to lazy-load the NotificationCenter ref if (!this.__notificationCenter) { this.__notificationCenter = this.refs.notificationCenter; } return this.__notificationCenter; } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } _detectOrientation() { //console.log('_detectOrientation', this.state.Width_Layout, this.state.Height_Layout); let H = this.state.Height_Layout + this.state.keyboardHeight; if(this.state.Width_Layout > H && this.state.orientation !== 'landscape') { this.setState({orientation: 'landscape'}); } else { this.setState({orientation: 'portrait'}); } } changeRoute(route, reason) { utils.timestampedLog('Change route', this.currentRoute, '->', route, 'with reason:', reason); let messages = this.state.messages; if (this.currentRoute === route) { if (route === '/ready') { if (this.state.selectedContact) { if (this.state.callContact) { if (this.state.callContact.uri !== this.state.selectedContact.uri && this.state.selectedContact.uri in messages) { delete messages[this.state.selectedContact.uri]; } } else { if (this.state.selectedContact.uri in messages) { delete messages[this.state.selectedContact.uri]; } } this.setState({ messages: messages, selectedContact: null, targetUri: '' }); } else { this.setState({ messages: {}, messageZoomFactor: 1 }); } } return; } else { if (route === '/ready' && this.state.selectedContact && Object.keys(this.state.messages).indexOf(this.state.selectedContact.uri) === -1) { this.getMessages(this.state.selectedContact.uri); } } if (route === '/conference') { this.backToForeground(); this.setState({inviteContacts: false}); } if (route === '/call') { this.backToForeground(); } if (route === '/ready' && reason !== 'back to home') { Vibration.cancel(); if (reason === 'conference_really_ended' && this.callKeeper.countCalls) { utils.timestampedLog('Change route cancelled because we still have calls'); return; } if (this.state.currentCall && reason === 'outgoing_connection_failed' && this.state.currentCall.direction === 'outgoing') { let target_uri = this.state.currentCall.remoteIdentity.uri.toLowerCase(); let options = {audio: true, video: true, participants: []} let streams = this.state.currentCall.getLocalStreams(); if (streams.length > 0) { let tracks = streams[0].getVideoTracks(); let mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; if (mediaType === 'audio') { options.video = false; } } this.setState({reconnectingCall: true}); console.log('Reconnecting call to', target_uri, 'with options', options); setTimeout(() => { if (target_uri.indexOf('@videoconference') > -1) { this.callKeepStartConference(target_uri, options); } else { this.callKeepStartCall(target_uri, options); } }, 5000); this.setState({ outgoingCallUUID: null, currentCall: null, selectedContacts: [], reconnectingCall: true, muted: false }); } else { if (this.state.callContact && this.state.callContact.uri in messages) { delete messages[this.state.callContact.uri]; } this.setState({ outgoingCallUUID: null, currentCall: null, callContact: null, messages: {}, selectedContact: null, inviteContacts: false, shareToContacts: false, selectedContacts: [], incomingCall: (reason === 'accept_new_call' || reason === 'user_hangup_call') ? this.state.incomingCall: null, reconnectingCall: false, muted: false }); } if (this.currentRoute === '/call' || this.currentRoute === '/conference') { if (reason !== 'user_hangup_call') { this.stopRingback(); InCallManager.stop(); } this.closeLocalMedia(); if (reason === 'accept_new_call') { if (this.state.incomingCall) { // then answer the new call if any let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } else if (reason === 'escalate_to_conference') { let conf_uri = []; conf_uri.push(this.state.accountId.split('@')[0]); this.participantsToInvite.forEach((p) => { conf_uri.push(p.split('@')[0]); }); conf_uri.sort(); let uri = conf_uri.toString().toLowerCase().replace(/,/g,'-') + '@' + config.defaultConferenceDomain; const options = {audio: this.outgoingMedia ? this.outgoingMedia.audio: true, video: this.outgoingMedia ? this.outgoingMedia.video: true, participants: this.participantsToInvite, skipHistory: true} this.participantsToInvite = []; this.callKeepStartConference(uri, options); } else { if (this.state.account && this._loaded) { setTimeout(() => { this.updateServerHistory('/ready') }, 1500); } } } if (reason === 'registered') { setTimeout(() => { this.updateServerHistory(reason) }, 1500); } if (reason === 'no_more_calls') { this.updateServerHistory(reason); this.updateLoading(null, 'incoming_call'); this.setState({incomingCallUUID: null}); } if (reason === 'start_up') { storage.get('account').then((account) => { if (account) { this.handleRegistration(account.accountId, account.password); } else { this.changeRoute('/login', 'start up'); } }); this.fetchSharedItems(); } } this.currentRoute = route; history.push(route); } componentWillUnmount() { utils.timestampedLog('App will unmount'); AppState.removeEventListener('change', this._handleAppStateChange); this._onFinishedPlayingSubscription.remove(); this._onFinishedLoadingSubscription.remove(); this._onFinishedLoadingURLSubscription.remove(); this._onFinishedLoadingFileSubscription.remove(); this.callKeeper.destroy(); this.closeConnection(); this._loaded = false; } get unmounted() { return !this._loaded; } isUnmounted() { return this.unmounted; } backPressed() { console.log('Back button pressed in route', this.currentRoute); if (this.state.incomingCallUUID) { this.hideInternalAlertPanel('backPressed'); return; } if (this.state.showQRCodeScanner) { this.toggleQRCodeScanner(); return; } if (this.currentRoute === '/call' || this.currentRoute === '/conference') { this.goBackToHome(); /* let call = this.state.currentCall || this.state.incomingCall; if (call && call.id) { this.hangupCall(call.id, 'user_hangup_call'); } */ } else if (this.currentRoute === '/ready') { if (this.state.selectedContact) { this.goBackToHome(); } else if (this.state.historyFilter) { this.filterHistory(null); } else { BackHandler.exitApp(); } } return true; } async componentDidMount() { utils.timestampedLog('App did mount'); //this.requestStoragePermission(); DeviceInfo.getFontScale().then((fontScale) => { this.setState({fontScale: fontScale}); }); this.keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', this._keyboardDidShow ); this.keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', this._keyboardDidHide ); BackHandler.addEventListener('hardwareBackPress', this.backPressed); // Start a timer that runs once after X milliseconds BackgroundTimer.runBackgroundTimer(() => { // this will be executed once after 10 seconds // even when app is the the background this.heartbeat(); }, 5000); try { await RNCallKeep.supportConnectionService (); utils.timestampedLog('Connection service is enabled'); } catch(err) { utils.timestampedLog(err); } try { await RNCallKeep.hasPhoneAccount(); utils.timestampedLog('Phone account is enabled'); } catch(err) { utils.timestampedLog(err); } if (Platform.OS === 'android') { RNDrawOverlay.askForDispalayOverOtherAppsPermission() .then(res => { //utils.timestampedLog("Display over other apps was granted"); // res will be true if permission was granted }) .catch(e => { utils.timestampedLog("Display over other apps was declined"); // permission was declined }) } // prime the ref //logger.debug('NotificationCenter ref: %o', this._notificationCenter); this._boundOnPushkitRegistered = this._onPushkitRegistered.bind(this); this._boundOnPushRegistered = this._onPushRegistered.bind(this); this._detectOrientation(); getPhoneNumber().then(phoneNumber => { this.setState({myPhoneNumber: phoneNumber}); }); this.listenforPushNotifications(); this.listenforSoundNotifications(); this._loaded = true; this.checkVersion(); } _keyboardDidShow(e) { this.setState({keyboardVisible: true, keyboardHeight: e.endCoordinates.height}); } _keyboardDidHide() { this.setState({keyboardVisible: false, keyboardHeight: 0}); } checkVersion() { if (Platform.OS === 'android') { getAppstoreAppMetadata("com.agprojects.sylk") //put any apps packageId here .then(metadata => { console.log("Sylk app version on playstore", metadata.version, "published on", metadata.currentVersionReleaseDate ); this.setState({appStoreVersion: metadata}); }) .catch(err => { console.log("error occurred", err); }); return; } else { getAppstoreAppMetadata("1489960733") //put any apps id here .then(appVersion => { console.log("Sylk app version on appstore", appVersion.version, "published on", appVersion.currentVersionReleaseDate); this.setState({appStoreVersion: appVersion}); }) .catch(err => { console.log("Error fetching app store version occurred", err); }); } } listenforSoundNotifications() { // Subscribe to event(s) you want when component mounted this._onFinishedPlayingSubscription = SoundPlayer.addEventListener('FinishedPlaying', ({ success }) => { //console.log('finished playing', success) }) this._onFinishedLoadingSubscription = SoundPlayer.addEventListener('FinishedLoading', ({ success }) => { //console.log('finished loading', success) }) this._onFinishedLoadingFileSubscription = SoundPlayer.addEventListener('FinishedLoadingFile', ({ success, name, type }) => { //console.log('finished loading file', success, name, type) }) this._onFinishedLoadingURLSubscription = SoundPlayer.addEventListener('FinishedLoadingURL', ({ success, url }) => { //console.log('finished loading url', success, url) }) } handleFirebasePushInForeground(parent) { // Must be outside of any component LifeCycle (such as `componentDidMount`). console.log('handleFirebasePushInForeground'); PushNotification.configure({ // (optional) Called when Token is generated (iOS and Android) onRegister: function (token) { //console.log("TOKEN:", token); }, // (required) Called when a remote is received or opened, or local notification is opened onNotification: function (notification) { // process the notification if (notification.userInteraction) { parent.handleFirebasePushInteraction(notification); } else { parent.handleFirebasePush(notification); } // (required) Called when a remote is received or opened, or local notification is opened notification.finish(PushNotificationIOS.FetchResult.NoData); }, // (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android) onAction: function (notification) { console.log("ACTION:", notification.action); console.log("NOTIFICATION:", notification); // process the action }, // (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS) onRegistrationError: function(err) { console.error(err.message, err); }, }); PushNotification.createChannel( { channelId: "sylk-messages", // (required) channelName: "My Sylk stream", // (required) channelDescription: "A channel to receive Sylk Message", // (optional) default: undefined. playSound: false, // (optional) default: true importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); PushNotification.createChannel( { channelId: "sylk-messages-sound", // (required) channelName: "My Sylk stream", // (required) channelDescription: "A channel to receive Sylk Message", // (optional) default: undefined. playSound: true, // (optional) default: true soundName: "default", // (optional) See `soundName` parameter of `localNotification` function importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); PushNotification.deleteChannel("sylk-alert-panel"); PushNotification.createChannel( { channelId: "sylk-alert-panel", // (required) channelName: "Sylk Incoming Calls", // (required) channelDescription: "Display alert panel for incoming calls", // (optional) default: undefined. importance: Importance.MAX, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. playSound: true, isRingtone: true // soundName: "incallmanager_ringtone.mp3" }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); /* console.log('Available Sylk channels:'); PushNotification.getChannels(function (channel_ids) { console.log(channel_ids); // ['channel_id_1'] }); */ } handleiOSNotification(notification) { // when user touches the system notification and app launches... console.log("Handle iOS push notification:", notification); } postAndroidMessageNotification(uri, content) { //https://www.npmjs.com/package/react-native-push-notification return; console.log('postAndroidMessageNotification'); PushNotification.localNotification({ /* Android Only Properties */ channelId: "sylk-messages", // (required) channelId, if the channel doesn't exist, notification will not trigger. showWhen: true, // (optional) default: true autoCancel: true, // (optional) default: true largeIcon: "ic_launcher", // (optional) default: "ic_launcher". Use "" for no large icon. largeIconUrl: "https://icanblink.com/apple-touch-icon-180x180.png", // (optional) default: undefined smallIcon: "", // (optional) default: "ic_notification" with fallback for "ic_launcher". Use "" for default small icon. bigText: content, // (optional) default: "message" prop subText: "New message", // (optional) default: none //bigPictureUrl: "https://www.example.tld/picture.jpg", // (optional) default: undefined bigLargeIcon: "ic_launcher", // (optional) default: undefined bigLargeIconUrl: "https://www.example.tld/bigicon.jpg", // (optional) default: undefined color: "red", // (optional) default: system default vibrate: true, // (optional) default: true vibration: 100, // vibration length in milliseconds, ignored if vibrate=false, default: 1000 priority: "high", // (optional) set notification priority, default: high ignoreInForeground: true, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting onlyAlertOnce: true, // (optional) alert will open only once with sound and notify, default: false invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true /* iOS and Android properties */ id: 0, // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID title: uri, // (optional) message: content, // (required) //picture: "https://www.example.tld/picture.jpg", // (optional) Display an picture with the notification, alias of `bigPictureUrl` for Android. default: undefined userInfo: {}, // (optional) default: {} (using null throws a JSON value '' error) playSound: false, // (optional) default: true soundName: "default", // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played) number: 10, // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero) repeatType: "day", // (optional) Repeating interval. Check 'Repeating Notifications' section for more info. }); } listenforPushNotifications() { if (this.state.appState === null) { this.setState({appState: 'active'}); } else { return; } if (Platform.OS === 'android') { Linking.getInitialURL().then((url) => { if (url) { utils.timestampedLog('Initial external URL: ' + url); this.eventFromUrl(url); } if (this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } else { this.changeRoute('/login', 'start_up'); } }).catch(err => { logger.error({ err }, 'Error getting external URL'); }); firebase.messaging().setBackgroundMessageHandler(async message => { this.handleFirebasePush(message); }); firebase.messaging().getToken() .then(fcmToken => { if (fcmToken) { this._onPushRegistered(fcmToken); } }); Linking.addEventListener('url', this.updateLinkingURL); } else if (Platform.OS === 'ios') { if (this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } else { this.changeRoute('/login', 'start_up'); } VoipPushNotification.addEventListener('register', this._boundOnPushkitRegistered); VoipPushNotification.registerVoipToken(); PushNotificationIOS.addEventListener('register', this._boundOnPushRegistered); PushNotificationIOS.addEventListener('localNotification', this.onLocalNotification); PushNotificationIOS.addEventListener('notification', this.onRemoteNotification); //let permissions = await checkIosPermissions(); //if (!permissions.alert) { PushNotificationIOS.requestPermissions(); //} } this.boundProximityDetect = this._proximityDetect.bind(this); DeviceEventEmitter.addListener('Proximity', this.boundProximityDetect); AppState.addEventListener('change', this._handleAppStateChange); if (Platform.OS === 'ios') { this._boundOnNotificationReceivedBackground = this._onNotificationReceivedBackground.bind(this); this._boundOnLocalNotificationReceivedBackground = this._onLocalNotificationReceivedBackground.bind(this); VoipPushNotification.addEventListener('notification', this._boundOnNotificationReceivedBackground); VoipPushNotification.addEventListener('localNotification', this._boundOnLocalNotificationReceivedBackground); } else if (Platform.OS === 'android') { this.handleFirebasePushInForeground(this); AppState.addEventListener('focus', this._handleAndroidFocus); AppState.addEventListener('blur', this._handleAndroidBlur); firebase .messaging() .requestPermission() .then(() => { // User has authorised }) .catch(error => { // User has rejected permissions }); this.messageListener = firebase .messaging() .onMessage((message: RemoteMessage) => { // this will just wake up the app to receive // the web-socket invite handled by this.incomingCall() this.handleFirebasePush(message); }); } } handleFirebasePushInteraction(notification) { let data = notification.data; let event = data.event; console.log("handleFirebasePushInteraction", event, data, 'in route', this.currentRoute); const callUUID = data['session-id']; const media = {audio: true, video: data['media-type'] === 'video'}; if (event === 'incoming_conference_request') { if (notification.action === 'Accept') { this.callKeepAcceptCall(callUUID); } else if (notification.action === 'Reject') { this.callKeepRejectCall(callUUID); } else if (notification.action === 'Dismiss') { this.dismissCall(callUUID); } } else if (event === 'incoming_session') { if (notification.action === 'Accept') { this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Video') { this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Audio') { media.video = false; this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Reject') { this.callKeepRejectCall(callUUID); } else if (notification.action === 'Dismiss') { this.dismissCall(callUUID); } } else if (event === 'message') { this.initialChatContact = data['from_uri']; } } handleFirebasePush(notification) { let event = notification.data.event; console.log("handleFirebasePush", event); const callUUID = notification.data['session-id']; const from = notification.data['from_uri']; const to = notification.data['to_uri']; const displayName = notification.data['from_display_name']; const outgoingMedia = {audio: true, video: notification.data['media-type'] === 'video'}; const mediaType = notification.data['media-type'] || 'audio'; if (this.unmounted) { //return; } if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); if (!from || !to) { return; } this.postAndroidIncomingCallNotification(notification.data); this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); if (!from) { return; } this.postAndroidIncomingCallNotification(notification.data); this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'cancel') { this.cancelIncomingCall(callUUID); } else if (event === 'message') { console.log('Push notification: new messages on Sylk server from', from); } } notifyIncomingMessageWhileInACall(from) { if (!this.state.selectedContact) { return; } if (this.state.selectedContact.uri !== from) { this._notificationCenter.postSystemNotification('New message from ' + from); this.vibrate(); return; } if (this.state.currentCall && this.state.currentCall.remoteIdentity.uri === from) { this.vibrate(); if (this.currentRoute !== '/ready') { this.goBackToHomeFromCall(); } return; } } sendLocalNotificationWithSound (){ console.log('sendLocalNotificationWithSound'); //PushNotificationIOS.addNotificationRequest({ PushNotificationIOS.presentLocalNotification({ id: 'notificationWithSound', title: 'Sample Title', subtitle: 'Sample Subtitle', body: 'Sample local notification with custom sound', sound: 'customSound.wav', badge: 1, }); }; sendNotification (title, subtitle, body) { DeviceEventEmitter.emit('remoteNotificationReceived', { remote: true, aps: { alert: {title: title, subtitle: subtitle, body: body}, sound: 'default', category: 'REACT_NATIVE', 'content-available': 1, 'mutable-content': 1, }, }); }; sendSilentNotification () { DeviceEventEmitter.emit('remoteNotificationReceived', { remote: true, aps: { category: 'REACT_NATIVE', 'content-available': 1, }, }); }; onRemoteNotification(notification) { const title = notification.getAlert().title; const subtitle = notification.getAlert().subtitle; const body = notification.getAlert().body; const message = notification.getMessage(); const content_available = notification.getContentAvailable(); const category = notification.getCategory(); const badge = notification.getBadgeCount(); const sound = notification.getSound(); const isClicked = notification.getData().userInteraction === 1; console.log('Got remote notification', title, subtitle, body); this.sendLocalNotification(title + ' ' + subtitle, body); }; sendLocalNotification (title, body) { PushNotificationIOS.presentLocalNotification({ alertTitle: title, alertBody: body }); }; onLocalNotification(notification) { //console.log('Got local notification', notification); this.updateTotalUread(); }; cancelIncomingCall(callUUID) { if (this.unmounted) { return; } this.hideInternalAlertPanel('cancel'); if (this.callKeeper._acceptedCalls.has(callUUID)) { return; } utils.timestampedLog('Push notification: cancel call', callUUID); let call = this.callKeeper._calls.get(callUUID); if (!call) { if (!this.callKeeper._cancelledCalls.has(callUUID)) { utils.timestampedLog('Cancel incoming call that did not arrive on web socket', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); if (this.startedByPush) { this.resetStartedByPush('cancelIncomingCall') if (this.currentRoute) { this.changeRoute('/ready', 'incoming_call_cancelled'); } else { this.changeRoute('/login', 'start_up'); } } this.updateLoading(null, 'cancel_incoming_call'); } return; } if (call.state === 'incoming') { utils.timestampedLog('Cancel incoming call that was not yet accepted', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); if (this.startedByPush) { if (this.currentRoute) { this.changeRoute('/ready', 'incoming_call_cancelled'); } else { this.changeRoute('/login', 'start_up'); } } } } _proximityDetect(data) { //utils.timestampedLog('Proximity changed, isNear is', data.isNear); if (!this.state.proximityEnabled) { return; } if (data.isNear) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } startCallWhenReady(targetUri, options) { this.resetGoToReadyTimer(); if (options.conference) { this.startConference(targetUri, options); } else { this.startCall(targetUri, options); } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } _onPushkitRegistered(token) { this.pushkittoken = token; } _onPushRegistered(token) { this.pushtoken = token; } _sendPushToken(account) { if ((this.pushtoken && !this.tokenSent)) { let token = null; //console.log('_sendPushToken this.pushtoken', this.pushtoken); if (Platform.OS === 'ios') { token = `${this.pushkittoken}-${this.pushtoken}`; } else if (Platform.OS === 'android') { token = this.pushtoken; } utils.timestampedLog('Push token for app', bundleId, 'sent'); account.setDeviceToken(token, Platform.OS, deviceId, true, bundleId); this.tokenSent = true; } } _handleAndroidFocus = nextFocus => { //utils.timestampedLog('----- APP in focus'); if (Platform.OS === 'ios') { PushNotificationIOS.cancelLocalNotifications(); } else { PushNotification.cancelAllLocalNotifications(); } this.setState({inFocus: true}); this.refreshNavigationItems(); this.fetchSharedItems(); this.respawnConnection(); } fetchSharedItems() { return; console.log('fetchSharedItems...'); ReceiveSharingIntent.getReceivedFiles(files => { // files returns as JSON Array example //[{ filePath: null, text: null, weblink: null, mimeType: null, contentUri: null, fileName: null, extension: null }] console.log(files); if (files.length > 0) { console.log('Will share to contacts', files); this.setState({shareToContacts: true, shareContent: files}); let item = files[0]; let what = 'Share text with contacts'; if (item.weblink) { what = 'Share web link with contacts'; } if (item.filePath) { what = 'Share file with contacts'; } this._notificationCenter.postSystemNotification(what); } else { console.log('Nothing to share'); } }, (error) =>{ console.log(error); }, 'ShareMedia' // share url protocol (must be unique to your app, suggest using your apple bundle id) ); } refreshNavigationItems() { var todayStart = new Date(); todayStart.setHours(0,0,0,0); var yesterdayStart = new Date(); yesterdayStart.setDate(yesterdayStart.getDate() - 2); yesterdayStart.setHours(0,0,0,0); let today = false; let yesterday = false; let conference = false; let navigationItems = this.state.navigationItems; Object.keys(this.state.myContacts).forEach((key) => { if (this.state.myContacts[key].tags.indexOf('conference') > -1 || this.state.myContacts[key].conference) { conference = true; } if (this.state.myContacts[key].timestamp > todayStart) { today = true; } if (this.state.myContacts[key].timestamp > yesterdayStart && this.state.myContacts[key].timestamp < todayStart) { yesterday = true; } }); navigationItems = {today: today, yesterday: yesterday, conference: conference}; this.setState({navigationItems: navigationItems}); } _handleAndroidBlur = nextBlur => { //utils.timestampedLog('----- APP out of focus'); this.setState({inFocus: false}); } _handleAppStateChange = nextAppState => { //utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (nextAppState === this.state.appState) { return; } if (this.callKeeper.countCalls === 0 && !this.state.outgoingCallUUID) { /* utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (this.callKeeper.countCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countCalls, 'calls'); } if (this.callKeeper.countPushCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countPushCalls, 'push calls'); } if (this.startedByPush) { utils.timestampedLog('- APP state changed, started by push in', nextAppState, 'state'); } if (this.state.connection) { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState, 'with connection', Object.id(this.state.connection)); } else { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState); } */ } if (this.state.appState === 'background' && nextAppState === 'active') { this.respawnConnection(nextAppState); } this.setState({appState: nextAppState}); } respawnConnection(state) { if (!this.state.connection) { utils.timestampedLog('Web socket does not exist'); } else if (!this.state.connection.state) { utils.timestampedLog('Web socket is waiting for connection...'); } else { /* if (this.state.connection.state !== 'ready' && this.state.connection.state !== 'connecting') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'reconnecting because', this.state.connection.state); this.state.connection.reconnect(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'new state is', this.state.connection.state); } */ } if (this.state.account) { if (!this.state.connection) { utils.timestampedLog('Active account without connection removed'); this.setState({account: null}); } } else { utils.timestampedLog('No active account'); } if (this.state.accountId && (!this.state.connection || !this.state.account)) { this.handleRegistration(this.state.accountId, this.state.password); } } closeConnection(reason='unmount') { if (!this.state.connection) { return; } if (!this.state.account && this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.connection.close(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.setState({connection: null, account: null}); } else if (this.state.connection && this.state.account) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.account.removeListener('outgoingCall', this.outgoingCall); this.state.account.removeListener('conferenceCall', this.outgoingConference); this.state.account.removeListener('incomingCall', this.incomingCallFromWebSocket); this.state.account.removeListener('missedCall', this.missedCall); this.state.account.removeListener('conferenceInvite', this.conferenceInviteFromWebSocket); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { utils.timestampedLog('Failed to remove account:', error); } else { //utils.timestampedLog('Account removed'); } if (this.state.connection) { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.state.connection.close(); } this.setState({connection: null, account: null}); } ); } else { this.setState({connection: null, account: null}); } } startCallFromCallKeeper(data) { utils.timestampedLog('Starting call from OS...'); let callUUID = data.callUUID || uuid.v4(); let is_conf = data.handle.search('videoconference.') === -1 ? false: true; this.backToForeground(); if (is_conf) { this.callKeepStartConference(data.handle, {audio: true, video: data.video || true, callUUID: callUUID}); } else { this.callKeepStartCall(data.handle, {audio: true, video: data.video, callUUID: callUUID}); } this._notificationCenter.removeNotification(); } selectContact(contact, origin='') { if (contact !== this.state.selectedContact) { this.setState({pinned: false}); } this.setState({selectedContact: contact}); this.initialChatContact = null; } logTimeline(step) { return; let diff = Math.floor((new Date() - this.startTimestamp) / 1000); console.log('Timeline:', step, diff); } connectionStateChanged(oldState, newState) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Web socket', connection, 'state changed:', oldState, '->' , newState); } this.logTimeline('connection ' + newState); switch (newState) { case 'closed': this.syncRequested = false; if (this.state.connection) { utils.timestampedLog('Web socket was terminated'); this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this._notificationCenter.postSystemNotification('Connection lost'); } //this.setState({connection: null, account: null}); this.setState({account: null}); break; case 'ready': this._notificationCenter.removeNotification(); this.updateLoading(null, 'ready'); if (this.state.autoLogin) { this.processRegistration(this.state.accountId, this.state.password); this.callKeeper.setAvailable(true); } break; case 'disconnected': this.syncRequested = false; if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { this.hangupCall(this.state.currentCall.id, 'outgoing_connection_failed'); } if (this.state.incomingCall) { this.hangupCall(this.state.incomingCall.id, 'connection_failed'); } this.setState({ registrationState: 'failed', generatedVideoTrack: false, }); if (this.currentRoute === '/login' && this.state.registrationKeepalive) { this.changeRoute('/ready', 'websocket disconnected'); } break; default: if (this.state.registrationKeepalive && !this.state.accountVerified) { this.updateLoading('Connecting...', 'connection'); } break; } } notificationCenter() { return this._notificationCenter; } showRegisterFailure(reason) { const connection = this.getConnection(); utils.timestampedLog('Registration error: ' + reason, 'on web socket', connection); this.setState({ registrationState: 'failed', status : { msg : 'Sign In failed: ' + reason, level : 'danger' } }); this.updateLoading(null, 'show_register_failure'); if (this.startedByPush) { // TODO: hangup incoming call } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'register failure'); } } registrationStateChanged(oldState, newState, data) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Registration state changed:', oldState, '->', newState, 'on web socket', connection); } if (!this.state.account) { utils.timestampedLog('Account', this.state.accountId, 'is disabled'); this.updateLoading(null, 'account_disabled'); return; } if (newState === 'failed') { let reason = data.reason; if (reason.indexOf('904') > -1) { // Sofia SIP: WAT reason = 'Wrong account or password'; } else if (reason === 408) { reason = 'Timeout'; } this.showRegisterFailure(reason); if (this.state.registrationKeepalive) { if (this.state.connection !== null && this.state.connection.state === 'ready') { utils.timestampedLog('Retry to register...'); this.state.account.register(); } } else { // add a timer to retry register after awhile if (reason >= 500 || reason === 408) { utils.timestampedLog('Retry to register after 5 seconds delay...'); setTimeout(this.state.account.register(), 5000); } else { if (this.registrationFailureTimer) { utils.timestampedLog('Cancel registration timer'); clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } } } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'register failed'); } } else if (newState === 'registered') { if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } if (!this.state.accountVerified) { this.loadSylkContacts(); } /* setTimeout(() => { this.updateServerHistory() }, 1000); */ if (this.state.enrollment) { let myContacts = this.state.myContacts; myContacts[this.state.account.id] = this.newContact(this.state.account.id, this.state.displayName); this.saveSylkContact(this.state.account.id, myContacts[this.state.account.id], 'enrollment'); } if (this.mustSendPublicKey) { var uri = uuid.v4() + '@' + this.state.defaultDomain; console.log('Send 1st public to', uri); this.sendPublicKey(uri); this.mustSendPublicKey = false; } storage.set('account', { accountId: this.state.account.id, password: this.state.password, verified: true }); this.setState({accountVerified: true, enrollment: false, autoLogin: true, registrationKeepalive: true, registrationState: 'registered' }); this.updateLoading(null, 'registered'); this.requestSyncConversations(this.state.lastSyncId); this.replayJournal(); //if (this.currentRoute === '/login' && (!this.startedByPush || Platform.OS === 'ios')) { // TODO if the call does not arrive, we never get back to ready if (this.currentRoute === '/login') { this.changeRoute('/ready', 'registered'); } return; } else { this.setState({status: null, registrationState: newState }); } if (this.mustLogout) { this.logout(); } } showAlertPanel(data) { console.log('Show alert panel'); if (this.callKeeper._cancelledCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._terminatedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._acceptedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._rejectedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } let contact; let media = {audio: true, video: false}; let callId; let from; let displayName; if ('from_display_name' in data && 'from_uri' in data) { // Firebase notification from = data.from_uri; displayName = data.from_display_name; callId = data['session-id']; if (data['media-type'] === 'video') { media.video = true; } } else if (data.hasOwnProperty('_remoteIdentity')) { // Sylk call object from = data.remoteIdentity.uri; displayName = data.remoteIdentity.displayName; callId = data.id; if (data.mediaTypes && data.mediaTypes.video) { media.video = true; } } else { console.log('Missing contact data for Alert panel'); return; } if (from in this.state.myContacts) { contact = this.state.myContacts[from]; } else { let contacts = this.lookupContacts(from); if (contacts.length > 0) { contact = this.newContact(from, contacts[0].name); } } if (!contact) { contact = this.newContact(from, displayName); } if (!callId) { console.log('Missing callId for Alert panel'); return; } this.setState({incomingCallUUID: callId, incomingContact: contact, incomingMedia: media }); } postAndroidIncomingCallNotification(data) { console.log('postAndroidIncomingCallNotification', data); if (Platform.OS !== 'android') { return; } if (this.callKeeper.selfManaged) { this.showAlertPanel(data); return; } let media = {audio: true, video: data['media-type'] === 'video'}; let from = data.from_display_name || data.from_uri; if (data.from_display_name && data.from_display_name != data.from_uri) { from = data.from_display_name + ' (' + data.from_uri + ')'; } console.log('Show Android incoming call notification', from, media); let actions = ['Audio']; if (media.video) { actions.push('Video'); } actions.push('Reject'); actions.push('Dismiss'); PushNotification.localNotification({ /* Android Only Properties */ channelId: "sylk-alert-panel", // (required) channelId, if the channel doesn't exist, notification will not trigger. vibrate: true, // (optional) default: true priority: "max", // (optional) set notification priority, default: high ongoing: true, loopSound: true, fullScreen: true, ignoreInForeground: false, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true actions: actions, /* iOS and Android properties */ title: 'Incoming call', // (optional) message: 'From ' + from, // (required) //picture: "https://www.example.tld/picture.jpg", // (optional) Display an picture with the notification, alias of `bigPictureUrl` for Android. default: undefined userInfo: data, // (optional) default: {} (using null throws a JSON value '' error) playSound: true, // (optional) default: true number: 10, // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero) }); } playIncomingRingtone(callUUID, force=false) { if (!this.callKeeper.selfManaged) { console.log('playIncomingRingtone skip because we are not self managed....'); return; } if (this.callKeeper._cancelledCalls.has(callUUID)) { console.log('playIncomingRingtone cancelled for', callUUID); return; } if (this.cancelRingtoneTimer) { clearTimeout(this.cancelRingtoneTimer); this.cancelRingtoneTimer = null; } else { console.log('Play local ringtone and vibrate'); Vibration.vibrate(VIBRATION_PATTERN, true); InCallManager.startRingtone('_DEFAULT_'); } this.cancelRingtoneTimer = setTimeout(() => { console.log('Cancel ringtones by timer') this.stopRingtones(); }, 60000); } stopRingtones() { if (this.cancelRingtoneTimer) { clearTimeout(this.cancelRingtoneTimer); this.cancelRingtoneTimer = null; } InCallManager.stopRingtone(); Vibration.cancel(); } hideInternalAlertPanel(by=null) { //console.log('hideInternalAlertPanel by', by); this.stopRingtones(); this.setState({incomingContact: null, incomingMedia: null}); } vibrate() { Vibration.vibrate(VIBRATION_PATTERN, true); setTimeout(() => { Vibration.cancel(); }, 1000); } heartbeat() { if (this.unmounted) { return; } this.heartbeats = this.heartbeats + 1; if (this.heartbeats % 40 == 0) { this.trimLogs(); } if (this.state.connection) { //console.log('Check calls in', this.state.appState, 'with connection', Object.id(this.state.connection), this.state.connection.state); } else { //console.log('Check calls in', this.state.appState, 'with no connection'); } let callState; if (this.state.currentCall && this.state.incomingCall && this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog('We have an incoming call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.incomingCall) { //utils.timestampedLog('We have an incoming call:', this.state.incomingCall ? (this.state.incomingCall.id + ' ' + this.state.incomingCall.state): 'None'); callState = this.state.incomingCall.state; } else if (this.state.currentCall) { //utils.timestampedLog('We have an outgoing call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.outgoingCallUUID) { //utils.timestampedLog('We have a pending outgoing call:', this.state.outgoingCallUUID); } else { //utils.timestampedLog('We have no calls'); if (this.state.appState === 'background' && this.state.connection && this.state.connection.state === 'ready') { //this.closeConnection('background with no calls'); } } this.callKeeper.heartbeat(); } stopRingback() { //utils.timestampedLog('Stop ringback'); InCallManager.stopRingback(); } resetGoToReadyTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; } } goToReadyNowAndCancelTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; this.changeRoute('/ready', 'cancel_timer_incoming_call'); } } isConference(call) { const _call = call || this.state.currentCall; if (_call && _call.hasOwnProperty('_participants')) { return true; } return false; } callStateChanged(oldState, newState, data) { if (this.unmounted) { return; } // outgoing accepted: null -> progress -> accepted -> established -> terminated // outgoing accepted: null -> progress -> established -> accepted -> terminated (with early media) // incoming accepted: null -> incoming -> accepted -> established -> terminated // 2nd incoming call is automatically rejected by sylkrtc library /* utils.timestampedLog('---currentCall start:', this.state.currentCall); utils.timestampedLog('---incomingCall start:', this.state.incomingCall); */ let call = this.callKeeper._calls.get(data.id); if (!call) { utils.timestampedLog("callStateChanged error: call", data.id, 'not found in callkeep manager'); return; } let callUUID = call.id; const connection = this.getConnection(); utils.timestampedLog('Sylkrtc call', callUUID, 'state change:', oldState, '->', newState, 'on web socket', connection); this.logTimeline('call ' + newState); /* if (newState === 'established' || newState === 'accepted') { // restore the correct UI state if it has transitioned illegally to /ready state if (call.hasOwnProperty('_participants')) { this.changeRoute('/conference', 'correct call state'); } else { this.changeRoute('/call', 'correct call state'); } } */ let newCurrentCall; let newincomingCall; let direction = call.direction; let hasVideo = false; let mediaType = 'audio'; let tracks; let readyDelay = 5000; if (this.state.incomingCall && this.state.currentCall) { if (newState === 'terminated') { if (this.state.incomingCall == this.state.currentCall) { newCurrentCall = null; newincomingCall = null; } if (this.state.incomingCall.id === call.id) { if (oldState === 'incoming') { //utils.timestampedLog('Call state changed:', 'incoming call must be cancelled'); this.hideInternalAlertPanel(newState); } if (oldState === 'established' || oldState === 'accepted') { //utils.timestampedLog('Call state changed:', 'incoming call ended'); this.hideInternalAlertPanel(newState); } // new call must be cancelled newincomingCall = null; newCurrentCall = this.state.currentCall; } if (this.state.currentCall != this.state.incomingCall && this.state.currentCall.id === call.id) { if (oldState === 'established' || newState === 'accepted') { //utils.timestampedLog('Call state changed:', 'outgoing call must be hangup'); // old call must be closed } newCurrentCall = null; newincomingCall = this.state.incomingCall; } } else if (newState === 'accepted') { if (this.state.incomingCall === this.state.currentCall) { newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { newCurrentCall = this.state.currentCall; } this.backToForeground(); } else if (newState === 'established') { if (this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog("Incoming call media started"); newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { //utils.timestampedLog("Outgoing call media started"); newCurrentCall = this.state.currentCall; } } else { //utils.timestampedLog('Call state changed:', 'We have two calls in unclear state'); } } else if (this.state.incomingCall) { //this.backToForeground(); //utils.timestampedLog('Call state changed: We have one incoming call'); newincomingCall = this.state.incomingCall; newCurrentCall = this.state.incomingCall; if (this.state.incomingCall.id === call.id) { if (newState === 'terminated') { if (this.startedByPush) { this.resetStartedByPush('terminated') this.requestSyncConversations(this.state.lastSyncId); } //utils.timestampedLog("Incoming call was cancelled"); this.hideInternalAlertPanel(newState); newincomingCall = null; newCurrentCall = null; readyDelay = 10; } else if (newState === 'accepted') { //utils.timestampedLog("Incoming call was accepted"); this.hideInternalAlertPanel(newState); this.backToForeground(); } else if (newState === 'established') { //utils.timestampedLog("Incoming call media started"); this.hideInternalAlertPanel(newState); } } } else if (this.state.currentCall) { //utils.timestampedLog('Call state changed: We have one current call'); newCurrentCall = newState === 'terminated' ? null : call; newincomingCall = null; if (newState !== 'terminated') { this.setState({reconnectingCall: false}); } } else { newincomingCall = null; newCurrentCall = null; } /* utils.timestampedLog('---currentCall:', newCurrentCall); utils.timestampedLog('---incomingCall:', newincomingCall); */ let callsState; switch (newState) { case 'progress': //this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; if (!this.isConference(call)){ InCallManager.startRingback('_BUNDLE_'); if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } else { this.speakerphoneOn(); } break; case 'early-media': //this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.stopRingback(); break; case 'established': callsState = this.state.callsState; callsState[callUUID] = {startTime: new Date()}; this.setState({callsState: callsState}); this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; InCallManager.start({media: mediaType}); if (direction === 'outgoing') { this.stopRingback(); if (this.state.speakerPhoneEnabled) { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } else { if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } break; case 'accepted': callsState = this.state.callsState; callsState[callUUID] = {startTime: new Date()}; this.setState({callsState: callsState}); if (direction === 'incoming') { this.callKeeper.setCurrentCallActive(callUUID); if (this.timeoutIncomingTimer) { clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } } if (callUUID === this.state.incomingCallUUID) { this.updateLoading(null, 'incoming_call'); } this.setState({incomingCallUUID: null}); this.backToForeground(); this.resetGoToReadyTimer(); if (direction === 'outgoing') { this.stopRingback(); } break; case 'terminated': let startTime; if (callUUID in this.state.callsState) { callsState = this.state.callsState; startTime = callsState[callUUID].startTime; delete callsState[callUUID]; this.setState({callsState: callsState}); } if (callUUID === this.state.incomingCallUUID) { this.setState({incomingCallUUID: null, incomingContact: null}); this.updateLoading(null, 'incoming_call'); } this._terminatedCalls.set(callUUID, true); utils.timestampedLog(callUUID, direction, 'terminated with reason', data.reason); if (this.state.incomingCall && this.state.incomingCall.id === call.id) { newincomingCall = null; } if (this.state.currentCall && this.state.currentCall.id === call.id) { newCurrentCall = null; } let callSuccesfull = false; let reason = data.reason; let play_busy_tone = !this.isConference(call); let CALLKEEP_REASON; let missed = false; let cancelled = false; let server_failure = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; if (!reason || reason.match(/200/)) { if (oldState === 'progress' && direction === 'outgoing') { reason = 'Cancelled'; cancelled = true; play_busy_tone = false; } else if (oldState === 'incoming') { reason = 'Cancelled'; missed = true; play_busy_tone = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else { reason = 'Hangup'; callSuccesfull = true; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } } else if (reason.match(/402/)) { reason = 'Payment required'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/403/)) { //reason = 'Forbidden'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/404/)) { reason = 'User not found'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/408/)) { reason = 'No answer'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/482/)) { reason = 'Loop detected'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/480/)) { reason = 'Is not online'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else if (reason.match(/486/)) { reason = 'Is busy'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/603/)) { reason = 'Cannot answer now'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/487/)) { reason = 'Cancelled'; play_busy_tone = false; cancelled = true; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } else if (reason.match(/488/)) { reason = 'Unacceptable media'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/4\d\d/)) { reason = 'Call failure: ' + reason; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/[5|6]\d\d/)) { reason = 'Server failure: ' + reason; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; server_failure = true; } else if (reason.match(/904/)) { // Sofia SIP: WAT reason = 'Wrong account or password'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else { server_failure = true; } if (play_busy_tone) { this.playBusyTone(); } if (direction === 'outgoing') { this.setState({declineReason: reason}); } this.stopRingback(); let msg; let current_datetime = new Date(); let formatted_date = utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); let diff = 0; if (startTime) { let duration = moment.duration(new Date() - startTime); diff = Math.floor((new Date() - startTime) / 1000); if (diff > 3600) { duration = duration.format('hh:mm:ss', {trim: false}); } else { duration = duration.format('mm:ss', {trim: false}); } msg = formatted_date + " - " + direction +" " + mediaType + " call ended after " + duration; this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); } else { msg = formatted_date + " - " + direction +" " + mediaType + " call ended (" + reason + ")"; if (!server_failure) { this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); if (reason.indexOf('PSTN calls forbidden') > -1) { setTimeout(() => { this.renderPurchasePSTNCredit(call.remoteIdentity.uri.toLowerCase()); }, 2000); } } } this.updateHistoryEntry(call.remoteIdentity.uri.toLowerCase(), callUUID, diff); this.callKeeper.endCall(callUUID, CALLKEEP_REASON); if (play_busy_tone && oldState !== 'established' && direction === 'outgoing') { this._notificationCenter.postSystemNotification('Call ended:', {body: reason}); } break; default: break; } /* utils.timestampedLog('---currentCall end:', newCurrentCall); utils.timestampedLog('---incomingCall end:', newincomingCall); */ this.setState({ currentCall: newCurrentCall, incomingCall: newincomingCall }); if (!this.state.currentCall && !this.state.incomingCall) { this.speakerphoneOn(); if (!this.state.reconnectingCall) { if (this.state.inFocus) { if (this.currentRoute !== '/ready') { utils.timestampedLog('Will go to ready in', readyDelay/1000, 'seconds (terminated)', callUUID); this.goToReadyTimer = setTimeout(() => { this.changeRoute('/ready', 'no_more_calls'); }, readyDelay); } } else { if (this.currentRoute !== '/conference') { this.changeRoute('/ready', 'no_more_calls'); } } } } if (this.state.currentCall) { //console.log('Current:', this.state.currentCall.id); } if (this.state.incomingCall) { //console.log('Incoming:', this.state.incomingCall.id); } } finishInviteToConference() { this.setState({inviteContacts: false, selectedContacts: []}); } goBackToCall() { let call = this.state.currentCall || this.state.incomingCall; if (call) { if (call.hasOwnProperty('_participants')) { this.changeRoute('/conference', 'back to call'); } else { this.changeRoute('/call', 'back to call'); } //this.getMessages(call.remoteIdentity.uri); } else { console.log('No call to go back to'); } } goBackToHome() { this.changeRoute('/ready', 'back to home'); } goBackToHomeFromCall() { this.changeRoute('/ready', 'back to home'); if (this.state.callContact) { this.setState({selectedContact: this.state.callContact}); if (Object.keys(this.state.messages).indexOf(this.state.callContact.uri) === -1) { this.getMessages(this.state.callContact.uri); } } } goBackToHomeFromConference() { this.changeRoute('/ready', 'back to home'); if (this.state.callContact) { this.setState({selectedContact: this.state.callContact}); if (Object.keys(this.state.messages).indexOf(this.state.callContact.uri) === -1) { this.getMessages(this.state.callContact.uri); } } } inviteToConference() { console.log('Invite contacts to conference...'); this.goBackToHome(); setTimeout(() => { this.setState({inviteContacts: true, selectedContacts: []}); }, 100); } handleEnrollment(account) { console.log('Enrollment for new account', account); this.signup[account.id] = account.email; storage.set('signup', this.signup); storage.set('last_signup', account.id); this.setState({displayName: account.displayName, enrollment: true, email: account.email}); this.handleRegistration(account.id, account.password); } handleRegistration(accountId, password) { //console.log('handleRegistration', accountId, 'verified =', this.state.accountVerified); if (this.state.account !== null && this.state.registrationState === 'registered' ) { return; } this.setState({ accountId : accountId, password : password, }); if (!this.startedByPush) { this.updateLoading('Connecting...', 'handleRegistration'); } if (this.state.accountVerified) { this.loadSylkContacts(); } if (this.state.connection === null) { utils.timestampedLog('Web socket handle registration for', accountId); const userAgent = 'Sylk Mobile'; let connection = sylkrtc.createConnection({server: config.wsServer}); utils.timestampedLog('Web socket', Object.id(connection), 'was opened'); connection.on('stateChanged', this.connectionStateChanged); connection.on('publicKey', this.publicKeyReceived); this.setState({connection: connection}); } else { if (this.state.connection.state === 'ready' && this.state.registrationState !== 'registered') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'handle registration for', accountId); this.processRegistration(accountId, password); } else if (this.state.connection.state !== 'ready') { if (this._notificationCenter) { this._notificationCenter.postSystemNotification('Waiting for Internet connection'); } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } } } } processRegistration(accountId, password, displayName) { if (!displayName) { displayName = this.state.displayName; } if (!this.state.connection) { return; } utils.timestampedLog('Process registration for', accountId, '(', displayName, ')'); if (this.state.account && this.state.connection) { this.state.connection.removeAccount(this.state.account, (error) => { this.setState({registrationState: null, registrationKeepalive: false}); } ); } const options = { account: accountId, password: password, displayName: displayName || '', incomingHeaderPrefixes: ['SSI'] }; if (this.state.connection._accounts.has(options.account)) { return; } if (this.state.accountVerified) { this.registrationFailureTimer = setTimeout(() => { this.showRegisterFailure('Register timeout'); this.processRegistration(accountId, password); }, 10000); } const account = this.state.connection.addAccount(options, (error, account) => { if (!error) { account.on('outgoingCall', this.outgoingCall); account.on('conferenceCall', this.outgoingConference); account.on('registrationStateChanged', this.registrationStateChanged); account.on('incomingCall', this.incomingCallFromWebSocket); account.on('incomingMessage', this.incomingMessage); account.on('syncConversations', this.syncConversations); account.on('readConversation', this.readConversation); account.on('removeConversation', this.removeConversation); account.on('removeMessage', this.removeMessage); account.on('outgoingMessage', this.outgoingMessage); account.on('messageStateChanged', this.messageStateChanged); account.on('missedCall', this.missedCall); account.on('conferenceInvite', this.conferenceInviteFromWebSocket); //utils.timestampedLog('Web socket account', account.id, 'is ready, registering...'); this._sendPushToken(account); this.setState({account: account}); this.generateKeysIfNecessary(account); account.register(); if (this.state.ssiRequired) { this.initSSIAgent(); + } else { + utils.timestampedLog('SSI wallet is disabled'); } storage.set('account', { accountId: this.state.accountId, password: this.state.password }); } else { this.showRegisterFailure(408); } }); } async initSSIAgent() { // SSI wallet - init agent with wallet Id this.state.accountId if (this.ssiAgent) { // already initialized return; } if (!this.state.accountId) { + utils.timestampedLog('Init SSI wallet failed because missing device account id'); return; } if (!this.state.myuuid) { - console.log('Missing UUID for SSI Agent'); + utils.timestampedLog('Init SSI wallet failed because missing device id'); return; } let walletId = this.state.accountId + '_' + this.state.myuuid.replace(/-/g, '_'); - console.log('Init SSI wallet...', walletId); + utils.timestampedLog('Init SSI wallet will be initialised', walletId); const BCOVRIN_TEST_GENESIS = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}` const agentConfig = { // The label is used for communication with other agents label: walletId, mediatorConnectionsInvite: 'https://http.mediator.community.animo.id?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNTZjMTlhYzgtYjc1YS00ODdmLWJiMjUtNTVkYTlhMTNmODVlIiwgImxhYmVsIjogIkFuaW1vIENvbW11bml0eSBNZWRpYXRvciIsICJzZXJ2aWNlRW5kcG9pbnQiOiAiaHR0cHM6Ly9odHRwLm1lZGlhdG9yLmNvbW11bml0eS5hbmltby5pZCIsICJyZWNpcGllbnRLZXlzIjogWyI5aUFkWlVqQTM2QTZ5YnRndFU2RmViY0JXM1J1cTM4QjZzaHBXY3RCV2RHZCJdfQ==', autoAcceptConnections: true, // logger: new ConsoleLogger(LogLevel.debug), autoAcceptCredentials: AutoAcceptCredential.Always, autoAcceptProofs: AutoAcceptProof.Always, walletConfig: { id: walletId, key: 'demo', // this must be autogenerated and stored for each sip account }, indyLedgers: [ { id: 'BCovrin Test', genesisTransactions: BCOVRIN_TEST_GENESIS, isProduction: false, }, ] }; this.ssiAgent = new Agent(agentConfig, agentDependencies); const httpOutboundTransporter = new HttpOutboundTransport(); this.ssiAgent.registerOutboundTransport(httpOutboundTransporter); const WsOutboundTransporter = new WsOutboundTransport(); this.ssiAgent.registerOutboundTransport(WsOutboundTransporter); try { await this.ssiAgent.initialize(); - console.log('SSI wallet initialised'); + utils.timestampedLog('SSI wallet initialised'); let ssiRoles = this.state.ssiRoles; this.ssiAgent.events.on(CredentialEventTypes.CredentialStateChanged, this.handleSSIAgentCredentialStateChange); this.ssiAgent.events.on(ConnectionEventTypes.ConnectionStateChanged, this.handleSSIAgentConnectionStateChange); if (ssiRoles.indexOf('verifier') === -1) { ssiRoles.push('verifier'); } const credentials = await this.ssiAgent.credentials.getAll(); - console.log('SSI wallet has', credentials.length, 'credentials'); + let hm = credentials.length > 0 ? credentials.length : "no"; + utils.timestampedLog('SSI wallet has', hm, 'credentials'); //console.log(credentials); if (credentials.length > 0) { - console.log('SSI added holder role'); + utils.timestampedLog('SSI added holder role'); if (ssiRoles.indexOf('holder') === -1) { ssiRoles.push('holder'); } } this.setState({ssiRoles: ssiRoles}); const allConnection = await this.ssiAgent.connections.getAll(); - console.log('SSI wallet has', allConnection.length, 'connections'); + utils.timestampedLog('SSI wallet has', allConnection.length, 'connections'); //console.log(allConnection); let noCred = credentials.length > 0 ? credentials.length : "no"; this._notificationCenter.postSystemNotification("SSI wallet initialised with " + noCred + " credentials"); const rmCommunityConnection = async () => { const connections = allConnection.filter(x => x.theirLabel === 'Animo Community Agent') for (const x of connections) { await this.ssiAgent.connections.deleteById(x.id) } } // await rmCommunityConnection() // run only once if (!allConnection.map((x) => x.theirLabel).includes('Animo Community Agent')) { // create a connection to Animo credential issuer, must be done once // connection is saved and reused later when we recreate de agent // once we do have a credential, we don't need to connect anymore await this.initSSIConnection(); } } catch (error) { - console.log('SSI wallet initialise error:', error); + utils.timestampedLog('SSI wallet initialise error:', error); } } async initSSIConnection() { // replaced by the QR code reader return; - console.log('SSI connection init'); + utils.timestampedLog('SSI connection init'); // this invitation should be obtained from a QR code from the issuer website // this is still demo with hardwired values -adi let url = 'https://didcomm.agent.community.animo.id?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNDdiNDE1ZjEtNDk3OS00OGM0LWI5YTMtYWM2OWZlMGM0ZjZiIiwgInJlY2lwaWVudEtleXMiOiBbIkJBMmt1N3FCQ2toZE5ud3N1cU5GS0ZQa2dNejZoMnA2TENDd2hIaEE3U0twIl0sICJsYWJlbCI6ICJBbmltbyBDb21tdW5pdHkgQWdlbnQiLCAic2VydmljZUVuZHBvaW50IjogImh0dHBzOi8vZGlkY29tbS5hZ2VudC5jb21tdW5pdHkuYW5pbW8uaWQifQ=='; try { const result = await this.ssiAgent.connections.receiveInvitationFromUrl(url); - console.log('SSI connection cached'); + utils.timestampedLog('SSI connection cached'); // now we can receive a credential from the issuer } catch (error) { - console.log('SSI connection error', error); + utils.timestampedLog('SSI connection error', error); } } async handleSSIAgentCredentialStateChange(event) { - console.log('SSI wallet Credential State Change', event.payload.credentialRecord.id, event.payload.previousState, '->', event.payload.credentialRecord.state); + utils.timestampedLog('SSI wallet Credential State Change', event.payload.credentialRecord.id, event.payload.previousState, '->', event.payload.credentialRecord.state); if (event.payload.credentialRecord.state === CredentialState.OfferReceived) { - console.log('SSI credential received:', event.payload.credentialRecord); + utils.timestampedLog('SSI credential received:', event.payload.credentialRecord); this._notificationCenter.postSystemNotification("New SSI credential received"); // this is not needed if we are configured to auto accept // this.ssiAgent.credentials.acceptOffer(event.payload.credentialRecord.id); } else if (event.payload.credentialRecord.state === CredentialState.Done) { - console.log('SSI wallet credential saved'); + utils.timestampedLog('SSI wallet credential saved'); this.postSystemNotification('SSI credential saved'); } } async handleSSIAgentConnectionStateChange(event) { - //console.log('SSI wallet connection', event.payload.connectionRecord.id, 'state changed to', event.payload.connectionRecord.state); + //utils.timestampedLog('SSI wallet connection', event.payload.connectionRecord.id, 'state changed to', event.payload.connectionRecord.state); } generateKeysIfNecessary(account) { let keyStatus = this.state.keyStatus; console.log('PGP key generation...'); if ('existsOnServer' in keyStatus) { //console.log('PGP key server was already queried'); // server was queried if (keyStatus['existsOnServer']) { this.setState({keyExistsOnServer: true}) if (keyStatus['existsLocal']) { // key exists in both places if (this.state.keys && keyStatus['serverPublicKey'] !== this.state.keys.public) { console.log('PGP key is different than the one on server'); this.setState({keyDifferentOnServer: true}); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } else { console.log('PGP key is the same as the one on server'); } } else { console.log('PGP key does not exist'); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } } else { if (!keyStatus['existsLocal']) { console.log('We have no PGP key here nor on server'); this.generateKeys(); } else { console.log('PGP key exists local but not on server'); } } } else { //console.log('PGP key server was not yet queried'); account.checkIfKeyExists((key) => { keyStatus['serverPublicKey'] = key; //console.log('PGP key server query done'); if (key) { keyStatus['existsOnServer'] = true; this.setState({keyExistsOnServer: true}) if (this.state.keys) { if (this.state.keys && this.state.keys.public !== key) { console.log('PGP key on server is different than ours'); this.setState({showImportPrivateKeyModal: true, keyDifferentOnServer: true}) } else { console.log('PGP key exists on server and we have it'); keyStatus['existsLocal'] = true; } } else { if (!this.state.contactsLoaded) { console.log('Wait for PGP key until contacts are loaded'); } else { console.log('We have no local PGP key'); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } } } else { keyStatus['existsOnServer'] = false; console.log('PGP key does not exist on server'); if (this.state.contactsLoaded) { this.generateKeys(); } else { console.log('Wait for PGP key until contacts are loaded'); } } this.setState({keyStatus: keyStatus}); }); } } setDevice(device) { const oldDevices = Object.assign({}, this.state.devices); if (device.kind === 'videoinput') { oldDevices['camera'] = device; } else if (device.kind === 'audioinput') { oldDevices['mic'] = device; } this.setState({devices: oldDevices}); storage.set('devices', oldDevices); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.getLocalMedia(); } getLocalMedia(mediaConstraints={audio: true, video: true}, nextRoute=null) { // eslint-disable-line space-infix-ops let callType = mediaConstraints.video ? 'video': 'audio'; utils.timestampedLog('Get local media for', callType, 'call'); const constraints = Object.assign({}, mediaConstraints); if (constraints.video === true) { if ((nextRoute === '/conference')) { constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; // TODO: remove this, workaround so at least safari works when joining a video conference } else if (nextRoute === '/conference' && isSafari) { constraints.video = false; } else { // ask for 720p video constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; } } logger.debug('getLocalMedia(), (modified) mediaConstraints=%o', constraints); navigator.mediaDevices.enumerateDevices() .then((devices) => { devices.forEach((device) => { //console.log(device); if ('video' in constraints && 'camera' in this.state.devices) { if (constraints.video && constraints.video !== false && (device.deviceId === this.state.devices.camera.deviceId || device.label === this.state.devices.camera.label)) { constraints.video.deviceId = { exact: device.deviceId }; } } if ('mic' in this.state.devices) { if (device.deviceId === this.state.devices.mic.deviceId || device.label === this.state.devices.mic.Label) { // constraints.audio = { // deviceId: { // exact: device.deviceId // } // }; } } }); }) .catch((error) => { utils.timestampedLog('Error: device enumeration failed:', error); }) .then(() => { return navigator.mediaDevices.getUserMedia(constraints) }) .then((localStream) => { clearTimeout(this.loadScreenTimer); //utils.timestampedLog('Local media acquired'); this.setState({localMedia: localStream}); if (nextRoute !== null) { this.changeRoute(nextRoute); } }) .catch((error) => { utils.timestampedLog('Access to local media failed, trying audio only', error); navigator.mediaDevices.getUserMedia({ audio: true, video: false }) .then((localStream) => { clearTimeout(this.loadScreenTimer); if (nextRoute !== null) { this.changeRoute(nextRoute, 'local media aquired'); } }) .catch((error) => { utils.timestampedLog('Access to local media failed:', error); clearTimeout(this.loadScreenTimer); this._notificationCenter.postSystemNotification("Can't access camera or microphone"); this.updateLoading(null, 'get_media'); this.changeRoute('/ready', 'local media failure'); }); }); } getConnection() { return this.state.connection ? Object.id(this.state.connection): null; } showConferenceModal() { Keyboard.dismiss(); this.setState({showConferenceModal: true}); } hideConferenceModal() { this.setState({showConferenceModal: false}); } async callKeepStartConference(targetUri, options={audio: true, video: true, participants: []}) { if (!targetUri) { return; } this.backToForeground(); this.resetGoToReadyTimer(); const micAllowed = await this.requestMicPermission(); if (!micAllowed) { console.log('Cannot start call without access to microphone'); return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } let callUUID = options.callUUID || uuid.v4(); let participants = options.participants || null; if (!options.skipHistory) { this.addHistoryEntry(targetUri, callUUID); } let participantsToInvite = []; if (participants) { participants.forEach((participant_uri) => { if (participant_uri === this.state.accountId) { return; } participantsToInvite.push(participant_uri); }); } this.outgoingMedia = options; this.setState({outgoingCallUUID: callUUID, reconnectingCall: false, callContact: this.state.selectedContact, participantsToInvite: participantsToInvite }); const media = options.video ? 'video' : 'audio'; if (participantsToInvite) { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri, 'with', participantsToInvite); } else { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri); } this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, conference: true, callUUID: callUUID}); } updateSelection(uri) { //console.log('updateSelection', uri); let selectedContacts = this.state.selectedContacts; //console.log('selectedContacts', selectedContacts); let idx = selectedContacts.indexOf(uri); if (idx === -1) { selectedContacts.push(uri); } else { selectedContacts.splice(idx, 1); } this.setState({selectedContacts: selectedContacts}); } async callKeepStartCall(targetUri, options) { this.resetGoToReadyTimer(); targetUri = targetUri.trim().toLowerCase(); if (targetUri.indexOf('@') === -1) { targetUri = targetUri + '@' + this.state.defaultDomain; } const micAllowed = await this.requestMicPermission(); if (!micAllowed) { console.log('Cannot start call without access to microphone'); return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } let callUUID = options.callUUID || uuid.v4(); this.setState({outgoingCallUUID: callUUID, reconnectingCall: false}); utils.timestampedLog('User will start call', callUUID, 'to', targetUri); this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, callUUID: callUUID}); setTimeout(() => { if (this.state.currentCall && this.state.currentCall.id === callUUID && this.state.currentCall.state === 'progress') { this.hangupCall(callUUID, 'cancelled_call'); } }, 60000); } startCall(targetUri, options) { this.setState({targetUri: targetUri, callContact: this.state.selectedContact}); this.getLocalMedia(Object.assign({audio: true, video: options.video}, options), '/call'); } timeoutCall(callUUID, uri) { utils.timestampedLog('Timeout answering call', callUUID); this.addHistoryEntry(uri, callUUID, direction='incoming'); this.forceUpdate(); } closeLocalMedia() { if (this.state.localMedia != null) { utils.timestampedLog('Close local media'); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.setState({localMedia: null}); } } async callKeepAcceptCall(callUUID, options={}) { // called from user interaction with Old alert panel // options used to be media to accept audio only but native panels do not have this feature this.hideInternalAlertPanel('accept'); const micAllowed = await this.requestMicPermission(); if (!micAllowed) { return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } this.logTimeline('accept call'); this.backToForeground(); this.callKeeper.acceptCall(callUUID, options); this.updateLoading(incomingCallLabel, 'incoming_call'); if (this.timeoutIncomingTimer) { clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } this.timeoutIncomingTimer = setTimeout(() => { this.updateLoading(null, 'incoming_call_timeout'); }, 45000); // TODO this timer must be cancelled if call arrives -adi } callKeepRejectCall(callUUID) { // called from user interaction with Old alert panel utils.timestampedLog('CallKeep will reject call', callUUID); this.hideInternalAlertPanel('reject'); this.callKeeper.rejectCall(callUUID); } dismissCall(callUUID) { // called from user interaction with Old alert panel this.hideInternalAlertPanel('dismiss'); } acceptCall(callUUID, options={}) { console.log('User accepted call', callUUID, options); this.hideInternalAlertPanel('accept'); this.backToForeground(); this.resetGoToReadyTimer(); if (this.state.currentCall) { utils.timestampedLog('Will hangup current call first'); this.hangupCall(this.state.currentCall.id, 'accept_new_call'); // call will continue after transition to /ready } else { utils.timestampedLog('Will get local media now'); let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; if ('video' in options) { hasVideo = hasVideo && options.video; } this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } rejectCall(callUUID) { // called by Call Keep when user rejects call utils.timestampedLog('User rejected call', callUUID); this.hideInternalAlertPanel('reject'); if (!this.state.currentCall) { this.changeRoute('/ready', 'rejected'); } if (this.state.incomingCall && this.state.incomingCall.id === callUUID) { utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', this.state.incomingCall.state, 'state'); this.state.incomingCall.terminate(); } } hangupCall(callUUID, reason) { utils.timestampedLog('Call', callUUID, 'hangup with reason:', reason); let call = this.callKeeper._calls.get(callUUID); let direction = null; let targetUri = null; if (call) { let direction = call.direction; utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', call.state, 'state'); call.terminate(); this.vibrate(); } if (this.busyToneInterval) { clearInterval(this.busyToneInterval); this.busyToneInterval = null; } if (reason === 'user_cancel_call' || reason === 'user_hangup_call' || reason === 'answer_failed' || reason === 'callkeep_hangup_call' || reason === 'accept_new_call' || reason === 'stop_preview' || reason === 'escalate_to_conference' || reason === 'user_hangup_conference_confirmed' || reason === 'timeout' || reason === 'outgoing_connection_failed' ) { this.setState({inviteContacts: false}); this.changeRoute('/ready', reason); if (reason === 'user_hangup_conference_confirmed') { if (this.conferenceEndedTimer) { console.log('Clear timer conferenceEndedTimer'); clearTimeout(this.conferenceEndedTimer); this.conferenceEndedTimer = null; } else { console.log('Clear timer conferenceEndedTimer is not needed'); } } } else if (reason === 'user_hangup_conference') { if (!this.conferenceEndedTimer ) { utils.timestampedLog('Save conference maybe?'); this.conferenceEndedTimer = setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } } else if (reason === 'user_cancelled_conference') { if (!this.conferenceEndedTimer ) { utils.timestampedLog('Save conference maybe?'); this.conferenceEndedTimer = setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } } else { utils.timestampedLog('Will go to ready in 6 seconds (hangup)'); setTimeout(() => { this.changeRoute('/ready', reason); }, 6000); } } playBusyTone() { //utils.timestampedLog('Play busy tone'); InCallManager.stop({busytone: '_BUNDLE_'}); } callKeepSendDtmf(digits) { utils.timestampedLog('Send DTMF', digits); if (this.state.currentCall) { this.callKeeper.sendDTMF(this.state.currentCall.id, digits); } } toggleProximity() { storage.set('proximityEnabled', !this.state.proximityEnabled); if (!this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } this.setState({proximityEnabled: !this.state.proximityEnabled}); } toggleMute(callUUID, muted) { if (this.state.muted != muted) { utils.timestampedLog('Toggle mute for call', callUUID, ':', muted); this.callKeeper.setMutedCall(callUUID, muted); this.setState({muted: muted}); } } async hideImportPrivateKeyModal() { this.setState({privateKey: null, privateKeyImportStatus: '', privateKeyImportSuccess: false, showImportPrivateKeyModal: false}); if (!this.state.keys && Object.keys(this.state.myContacts).length === 0) { this.addTestContacts(); } } async showImportPrivateKeyModal(force=false) { let keyStatus = this.state.keyStatus; if (force) { console.log('Force show PGP key import'); this.setState({showImportPrivateKeyModal: true}); } else { if ('existsOnServer' in keyStatus) { if ('existsLocal' in keyStatus) { if (!keyStatus['existsLocal']) { this.setState({showImportPrivateKeyModal: true}); } else { console.log('PGP key exists locally'); } } else { console.log('PGP key was not checked locally'); } } else { console.log('PGP key was not checked on server'); } } } async hideExportPrivateKeyModal() { this.setState({privateKey: null, showExportPrivateKeyModal: false}); } async showExportPrivateKeyModal() { this.setState({showExportPrivateKeyModal: true}); } togglePinned() { console.log('togglePinned', this.state.selectedContact); if (this.state.selectedContact) { this.getMessages(this.state.selectedContact.uri, !this.state.pinned); this.setState({pinned: !this.state.pinned}); } } toggleSSI() { // user setting to enable/disable ssiAgent let ssiRequired = !this.state.ssiRequired; console.log('toggleSSI to', ssiRequired); this.setState({ssiRequired: ssiRequired}); if (ssiRequired) { this.initSSIAgent(); } else { this.setState({ssiAgent: null}); } storage.set('ssi', {required: ssiRequired}); } toggleSpeakerPhone() { if (this.state.speakerPhoneEnabled === true) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } toggleCallMeMaybeModal() { this.setState({showCallMeMaybeModal: !this.state.showCallMeMaybeModal}); } toggleQRCodeScanner() { - utils.timestampedLog('Toggle QR code scanner'); + //utils.timestampedLog('Toggle QR code scanner'); this.setState({showQRCodeScanner: !this.state.showQRCodeScanner}); } async handleSSIEnrolment(url) { if (!this.ssiAgent) { console.log('No SSI agent available for handling enrolment to', url); return; } - console.log('SSI enrolment invitation URL', url); + utils.timestampedLog('SSI enrolment invitation URL', url); try { const ssiConnectionRecord = await this.ssiAgent.connections.receiveInvitationFromUrl(url); - console.log('SSI enrolment requested', ssiConnectionRecord.id); + utils.timestampedLog('SSI enrolment requested', ssiConnectionRecord.id); setTimeout(() => { this._notificationCenter.postSystemNotification('SSI enrolment requested'); }, 2000); } catch (error) { utils.timestampedLog('SSI enrolment error', error); setTimeout(() => { this._notificationCenter.postSystemNotification('SSI enrolment ' + error); }, 2000); } } speakerphoneOn() { utils.timestampedLog('Speakerphone On'); this.setState({speakerPhoneEnabled: true}); InCallManager.setForceSpeakerphoneOn(true); let call = this.state.currentCall || this.state.incomingCall; if (call) { RNCallKeep.toggleAudioRouteSpeaker(call.id, true); } } speakerphoneOff() { utils.timestampedLog('Speakerphone Off'); this.setState({speakerPhoneEnabled: false}); InCallManager.setForceSpeakerphoneOn(false); let call = this.state.currentCall || this.state.incomingCall; if (call) { RNCallKeep.toggleAudioRouteSpeaker(call.id, false); } } startGuestConference(targetUri) { this.setState({targetUri: targetUri}); this.getLocalMedia({audio: true, video: true}); } outgoingCall(call) { // called by sylkrtc.js when an outgoing call starts call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); this.callKeeper.startOutgoingCall(call); this.updateLoading(null, 'outgoing_call'); } outgoingConference(call) { // called by sylrtc.js when an outgoing conference starts call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); this.callKeeper.startOutgoingCall(call); this.updateLoading(null, 'outgoing_call'); } _onLocalNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); utils.timestampedLog('Handle local iOS PUSH notification: ', notificationContent); } _onNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); const event = notificationContent['event']; const callUUID = notificationContent['session-id']; const to = notificationContent['to_uri']; const from = notificationContent['from_uri']; const displayName = notificationContent['from_display_name']; const outgoingMedia = {audio: true, video: notificationContent['media-type'] === 'video'}; const mediaType = notificationContent['media-type'] || 'audio'; /* * Local Notification Payload * * - `alertBody` : The message displayed in the notification alert. * - `alertAction` : The "action" displayed beneath an actionable notification. Defaults to "view"; * - `soundName` : The sound played when the notification is fired (optional). * - `category` : The category of this notification, required for actionable notifications (optional). * - `userInfo` : An optional object containing additional notification data. */ if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); this.startedByPush = true; this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); this.startedByPush = true; this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'cancel') { utils.timestampedLog('Push notification: cancel call', callUUID); VoipPushNotification.presentLocalNotification({alertBody:'Call cancelled'}); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); this.resetStartedByPush('cancel'); } else if (event === 'message') { utils.timestampedLog('Push for messages received'); VoipPushNotification.presentLocalNotification({alertBody:'Messages received'}); } /* if (notificationContent['event'] === 'incoming_session') { VoipPushNotification.presentLocalNotification({ alertBody:'Incoming ' + notificationContent['media-type'] + ' call from ' + notificationContent['from_display_name'] }); } */ if (VoipPushNotification.wakeupByPush) { utils.timestampedLog('We wake up by push notification'); VoipPushNotification.wakeupByPush = false; VoipPushNotification.onVoipNotificationCompleted(callUUID); } } backToForeground() { //console.log('backToForeground...'); if (this.state.appState !== 'active') { this.callKeeper.backToForeground(); } if (this.state.accountId) { this.handleRegistration(this.state.accountId, this.state.password); } PushNotification.popInitialNotification((notification) => { if (notification) { console.log('Initial push notification', notification); } }); } incomingConference(callUUID, to, from, displayName, outgoingMedia={audio: true, video: true}) { if (this.unmounted) { return; } const mediaType = outgoingMedia.video ? 'video' : 'audio'; utils.timestampedLog('Incoming', mediaType, 'conference invite from', from, displayName, 'to room', to); if (this.state.account && from === this.state.account.id) { utils.timestampedLog('Reject conference call from myself', callUUID); this.callKeeper.rejectCall(callUUID); return; } if (this.autoRejectIncomingCall(callUUID, from, to)) { return; } let incomingContact = this.newContact(from, displayName); this.setState({incomingCallUUID: callUUID, incomingContact: incomingContact}); this.callKeeper.handleConference(callUUID, to, from, displayName, mediaType, outgoingMedia); } startConference(targetUri, options={audio: true, video: true, participants: []}) { this.backToForeground(); this.updateLoading(null, 'start_conference'); utils.timestampedLog('New outgoing conference to room', targetUri); this.setState({targetUri: targetUri}); this.getLocalMedia({audio: options.audio, video: options.video}, '/conference'); this.getMessages(targetUri); } escalateToConference(participants) { let outgoingMedia = {audio: true, video: true}; let mediaType = 'video'; let call; this.setState({selectedContacts: []}); if (this.state.currentCall) { call = this.state.currentCall; } else if (this.state.incomingCall) { call = this.state.currentCall; } else { console.log('No call to escalate'); return } const localStreams = call.getLocalStreams(); if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; if (localStream.getVideoTracks().length == 0) { outgoingMedia.video = false; mediaType = 'audio'; } } this.outgoingMedia = outgoingMedia; this.participantsToInvite = participants; console.log('Escalate', mediaType, 'call', call.id, 'to conference with', participants.toString()); this.hangupCall(call.id, 'escalate_to_conference'); } conferenceInviteFromWebSocket(data) { // comes from web socket utils.timestampedLog('Conference invite from websocket', data.id, 'from', data.originator, 'for room', data.room); if (this.isConference()) { return; } //this._notificationCenter.postSystemNotification('Expecting conference invite', {body: `from ${data.originator.displayName || data.originator.uri}`}); } updateLinkingURL = (event) => { // this handles the use case where the app is running in the background and is activated by the listener... //console.log('Updated Linking url', event.url); this.eventFromUrl(event.url); DeepLinking.evaluateUrl(event.url); } eventFromUrl(url) { url = decodeURI(url); try { let direction; let event; let callUUID; let from; let to; let displayName; let mediaType = 'audio'; var url_parts = url.split("/"); let scheme = url_parts[0]; //console.log(url_parts); if (scheme === 'sylk:') { //sylk://conference/incoming/callUUID/from/to/media - when Android is asleep //sylk://call/outgoing/callUUID/to/displayName - from system dialer/history //sylk://call/incoming/callUUID/from/to/displayName/media - when Android is asleep //sylk://call/cancel//callUUID - when Android is asleep event = url_parts[2]; direction = url_parts[3]; callUUID = url_parts[4]; from = url_parts[5]; to = url_parts[6]; displayName = url_parts[7]; mediaType = url_parts[8] || 'audio'; if (event !== 'cancel' && from && from.search('@videoconference.') > -1) { event = 'conference'; to = from; } this.setState({targetUri: from}); } else if (scheme === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link direction = 'outgoing'; event = url_parts[3]; to = url_parts[4]; callUUID = uuid.v4(); if (to.indexOf('@') === -1 && event === 'conference') { to = url_parts[4] + '@' + config.defaultConferenceDomain; } else if (to.indexOf('@') === -1 && event === 'call') { to = url_parts[4] + '@' + this.state.defaultDomain; } this.setState({targetUri: to}); } let data = {}; data['session-id'] = callUUID; data['event'] = event; data['to_uri'] = to; data['from_uri'] = from; data['from_display_name'] = displayName; data['media-type'] = mediaType; if (event === 'conference') { utils.timestampedLog('Conference from external URL:', url); this.startedByPush = true; if (direction === 'outgoing' && to) { utils.timestampedLog('Outgoing conference to', to); this.backToForeground(); this.callKeepStartConference(to, {audio: true, video: true, callUUID: callUUID}); } else if (direction === 'incoming' && from) { utils.timestampedLog('Incoming conference from', from); // allow app to wake up this.backToForeground(); const media = {audio: true, video: mediaType === 'video'} this.postAndroidIncomingCallNotification(data); this.incomingConference(callUUID, to, from, displayName, media); } } else if (event === 'call') { this.startedByPush = true; if (direction === 'outgoing') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Outgoing call to', from); this.backToForeground(); this.callKeepStartCall(from, {audio: true, video: false, notification: callUUID}); } else if (direction === 'incoming') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Incoming', mediaType, 'call from', from); //this.playIncomingRingtone(callUUID, true); this.postAndroidIncomingCallNotification(data); this.incomingCallFromPush(callUUID, from, displayName, mediaType, true); } else if (direction === 'cancel') { this.cancelIncomingCall(callUUID); } } else { utils.timestampedLog('Error: Invalid external URL event', event); } } catch (err) { utils.timestampedLog('Error parsing URL', url, ":", err); } } autoRejectIncomingCall(callUUID, from, to) { //utils.timestampedLog('Check auto reject call from', from); if (this.state.blockedUris) { if (this.state.blockedUris.indexOf(from) > -1 || (this.state.blockedUris.indexOf('anonymous@anonymous.invalid') > -1 && (from === 'anonymous@anonymous.invalid' || from.indexOf('@guest.') > -1))) { utils.timestampedLog('Reject call', callUUID, 'from blocked URI', from); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from ${from}`}); return true; } } const fromDomain = '@' + from.split('@')[1] if (this.state.blockedUris && this.state.blockedUris.indexOf(fromDomain) > -1) { utils.timestampedLog('Reject call', callUUID, 'from blocked domain', fromDomain); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from domain ${fromDomain}`}); return true; } if (this.state.currentCall && this.state.incomingCall && this.state.currentCall === this.state.incomingCall && this.state.incomingCall.id !== callUUID) { utils.timestampedLog('Reject second incoming call'); this.callKeeper.rejectCall(callUUID); } if (this.state.account && from === this.state.account.id && this.state.currentCall && this.state.currentCall.remoteIdentity.uri === from) { utils.timestampedLog('Reject call to myself', callUUID); this.callKeeper.rejectCall(callUUID); return true; } if (this._terminatedCalls.has(callUUID)) { utils.timestampedLog('Reject call already terminated', callUUID); this.cancelIncomingCall(callUUID); return true; } if (this.isConference()) { utils.timestampedLog('Reject call while in a conference', callUUID); if (to !== this.state.targetUri) { this._notificationCenter.postSystemNotification('Missed call from', {body: from}); } this.callKeeper.rejectCall(callUUID); return true; } if (this.state.currentCall && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri !== from) { utils.timestampedLog('Reject call while outgoing in progress', callUUID); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Missed call from', {body: from}); return true; } return false; } autoAcceptIncomingCall(callUUID, from) { // TODO: handle ping pong where we call each other back if (this.state.currentCall && this.state.currentCall.direction === 'outgoing' && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri === from) { this.hangupCall(this.state.currentCall.id, 'accept_new_call'); this.setState({currentCall: null}); utils.timestampedLog('Auto accept incoming call from same address I am calling', callUUID); return true; } return false; } incomingCallFromPush(callUUID, from, displayName, mediaType, force) { //utils.timestampedLog('Handle incoming PUSH call', callUUID, 'from', from, '(', displayName, ')'); if (this.unmounted) { return; } if (this.autoRejectIncomingCall(callUUID, from)) { return; } if (this.autoAcceptIncomingCall(callUUID, from)) { return; } this.backToForeground(); this.goToReadyNowAndCancelTimer(); this.setState({targetUri: from}); let skipNativePanel = false; if (!this.callKeeper._calls.get(callUUID) || (this.state.currentCall && this.state.currentCall.direction === 'outgoing')) { //this._notificationCenter.postSystemNotification('Incoming call', {body: `from ${from}`}); if (Platform.OS === 'android' && this.state.appState === 'foreground') { skipNativePanel = true; } } this.callKeeper.incomingCallFromPush(callUUID, from, displayName, mediaType, force, skipNativePanel); } incomingCallFromWebSocket(call, mediaTypes) { if (this.unmounted) { return; } if (this.timeoutIncomingTimer) { console.log('Clear incoming timer'); clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } this.callKeeper.addWebsocketCall(call); const callUUID = call.id; const from = call.remoteIdentity.uri; //this.playIncomingRingtone(callUUID); //utils.timestampedLog('Handle incoming web socket call', callUUID, 'from', from, 'on connection', Object.id(this.state.connection)); // because of limitation in Sofia stack, we cannot have more then two calls at a time // we can have one outgoing call and one incoming call but not two incoming calls // we cannot have two incoming calls, second one is automatically rejected by sylkrtc.js if (this.autoRejectIncomingCall(callUUID, from)) { return; } const autoAccept = this.autoAcceptIncomingCall(callUUID, from); this.goToReadyNowAndCancelTimer(); call.mediaTypes = mediaTypes; call.on('stateChanged', this.callStateChanged); this.setState({incomingCall: call}); let skipNativePanel = false; if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { if (Platform.OS === 'android') { this.showAlertPanel(call); skipNativePanel = true; } } console.log('Incoming call extra headers', call.headers); this.callKeeper.incomingCallFromWebSocket(call, autoAccept, skipNativePanel); } missedCall(data) { utils.timestampedLog('Missed call from ' + data.originator.uri, '(', data.originator.displayName, ')'); /* let msg; let current_datetime = new Date(); let formatted_date = utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); msg = formatted_date + " - missed call"; this.saveSystemMessage(data.originator.uri.toLowerCase(), msg, 'incoming', true); */ if (!this.state.currentCall) { let from = data.originator.displayName || data.originator.uri; this._notificationCenter.postSystemNotification('Missed call', {body: `from ${from}`}); if (Platform.OS === 'ios') { VoipPushNotification.presentLocalNotification({alertBody:'Missed call from ' + from}); } } this.updateServerHistory('missedCall') } updateServerHistory(from) { //console.log('updateServerHistory by', from); //this.contactsCount(); if (this.state.serverHistoryUpdatedBy === 'registered' && from === 'syncConversations') { // Avoid double query at start return; } if (this.state.serverHistoryUpdatedBy === 'syncConversations' && from === 'registered') { // Avoid double query at start return; } this.setState({serverHistoryUpdatedBy: from}) if (!this.state.contactsLoaded) { return; } if (!this.state.firstSyncDone) { return; } if (this.currentRoute === '/ready') { this.setState({refreshHistory: !this.state.refreshHistory}); } } startPreview() { this.getLocalMedia({audio: true, video: true}, '/preview'); } sendPublicKey(uri) { if (!uri) { console.log('Missing uri, cannot send public key'); } if (uri === this.state.accountId) { return; } // Send outgoing messages if (this.state.account && this.state.keys && this.state.keys.public) { console.log('Sending public key to', uri); this.state.account.sendMessage(uri, this.state.keys.public, 'text/pgp-public-key'); } else { console.log('No public key available'); } } async saveOutgoingRawMessage(id, from_uri, to_uri, content, contentType) { let timestamp = new Date(); let params; let unix_timestamp = Math.floor(timestamp / 1000); params = [this.state.accountId, id, JSON.stringify(timestamp), unix_timestamp, content, contentType, from_uri, to_uri, "outgoing", "1"]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL insert message OK'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } showCallMeModal() { this.setState({showCallMeMaybeModal: true}); setTimeout(() => { this.hideCallMeModal(); }, 25000); } hideCallMeModal() { this.setState({showCallMeMaybeModal: false}); } async saveSylkContact(uri, contact, origin=null) { if (!contact) { contact = this.newContact(uri); } else { contact = this.sanitizeContact(uri, contact, 'saveSylkContact'); } if (!contact) { return; } console.log('Save Sylk Contact', uri, contact.name, 'by', origin); if (this.sql_contacts_keys.indexOf(uri) > -1) { this.updateSylkContact(uri, contact, origin); return; } let unread_messages = contact.unread.toString(); if (origin === 'saveIncomingMessage' && this.state.selectedContact && this.state.selectedContact.uri === uri) { unread_messages = ''; console.log('Do not update unread messages for', uri); } let conference = contact.conference ? 1: 0; let tags = contact.tags.toString(); let media = contact.lastCallMedia.toString(); let participants = contact.participants.toString(); let unixTime = Math.floor(contact.timestamp / 1000); let params = [this.state.accountId, contact.email, contact.photo, unixTime, uri, contact.name || '', contact.organization || '', unread_messages || '', tags || '', participants || '', contact.publicKey || '', contact.direction, media, conference, contact.lastCallId, contact.lastCallDuration]; await this.ExecuteQuery("INSERT INTO contacts (account, email, photo, timestamp, uri, name, organization, unread_messages, tags, participants, public_key, direction, last_call_media, conference, last_call_id, last_call_duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { if (result.rowsAffected === 1) { //console.log('SQL inserted contact', contact.uri, 'by', origin); } this.sql_contacts_keys.push(uri); let myContacts = this.state.myContacts; if (uri !== this.state.accountId) { myContacts[uri] = contact; let favorite = myContacts[uri].tags.indexOf('favorite') > -1 ? true: false; let blocked = myContacts[uri].tags.indexOf('blocked') > -1 ? true: false; this.updateFavorite(uri, favorite); this.updateBlocked(uri, blocked); this.setState({myContacts: myContacts}); } else { this.setState({email: contact.email, displayName: contact.name}) if (myContacts[uri].tags.indexOf('chat') > -1 || myContacts[uri].tags.indexOf('history') > -1) { myContacts[uri] = contact; this.setState({myContacts: myContacts}); } } }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') > -1) { //console.log('SQL insert contact failed, try update', uri); this.updateSylkContact(uri, contact, origin); } else { //console.log('SQL insert contact', uri, 'error:', error); //console.log('Existing keys during insert:', this.sql_contacts_keys); } }); } async updateSylkContact(uri, contact, origin=null) { //console.log('updateSylkContact', contact); let unixTime = Math.floor(contact.timestamp / 1000); let unread_messages = contact.unread.toString(); let media = contact.lastCallMedia.toString(); let tags = contact.tags.toString(); let conference = contact.conference ? 1: 0; let participants = contact.participants.toString(); let params = [contact.photo, contact.email, contact.lastMessage, contact.lastMessageId, unixTime, contact.name || '', contact.organization || '', unread_messages || '', contact.publicKey || '', tags, participants, contact.direction, media, conference, contact.lastCallId, contact.lastCallDuration, contact.uri, this.state.accountId]; await this.ExecuteQuery("UPDATE contacts set photo = ?, email = ?, last_message = ?, last_message_id = ?, timestamp = ?, name = ?, organization = ?, unread_messages = ?, public_key = ?, tags = ? , participants = ?, direction = ?, last_call_media = ?, conference = ?, last_call_id = ?, last_call_duration = ? where uri = ? and account = ?", params).then((result) => { if (result.rowsAffected === 1) { //console.log('SQL updated contact', contact.uri, 'by', origin); } let myContacts = this.state.myContacts; if (uri !== this.state.accountId) { myContacts[uri] = contact; let favorite = myContacts[uri].tags.indexOf('favorite') > -1 ? true: false; let blocked = myContacts[uri].tags.indexOf('blocked') > -1 ? true: false; this.updateFavorite(uri, favorite); this.updateBlocked(uri, blocked); this.setState({myContacts: myContacts}); } else { this.setState({email: contact.email, displayName: contact.name}) if (myContacts[uri].tags.indexOf('chat') > -1 || myContacts[uri].tags.indexOf('history') > -1) { myContacts[uri] = contact; this.setState({myContacts: myContacts}); } } }).catch((error) => { console.log('SQL update contact', uri, 'error:', error); }); } async deleteSylkContact(uri) { if (uri === this.state.accountId) { await this.ExecuteQuery("UPDATE contacts set direction = null, last_message = null, last_message_id = null, unread_messages = '' where account = ? and uri = ?", [uri, uri]).then((result) => { console.log('SQL update my own contact'); let myContacts = this.state.myContacts; if (uri in myContacts) { delete myContacts[uri]; this.setState({myContacts: myContacts}); } }).catch((error) => { console.log('Delete update mysql SQL error:', error); }); } else { await this.ExecuteQuery("DELETE from contacts where uri = ? and account = ?", [uri, this.state.accountId]).then((result) => { if (result.rowsAffected > 0) { console.log('SQL deleted contact', uri); } let myInvitedParties = this.state.myInvitedParties; if (uri in myInvitedParties) { delete myInvitedParties[uri]; this.setState({myInvitedParties: myInvitedParties}); } let idx = this.sql_contacts_keys.indexOf(uri); if (idx > -1) { this.sql_contacts_keys.splice(idx, 1); } //console.log('new keys after delete', this.sql_contacts_keys); let myContacts = this.state.myContacts; if (uri in myContacts) { delete myContacts[uri]; this.setState({myContacts: myContacts}); } }).catch((error) => { console.log('Delete contact SQL error:', error); }); } } async replicatePrivateKey(password) { if (!this.state.account) { console.log('No account'); return; } if (!this.state.keys || !this.state.keys.private) { return; } password = password.trim(); const public_key = this.state.keys.public.replace(/\r/g, '').trim(); await OpenPGP.encryptSymmetric(this.state.keys.private, password, KeyOptions).then((encryptedBuffer) => { utils.timestampedLog('Sending encrypted private key'); encryptedBuffer = public_key + "\n" + encryptedBuffer; this.state.account.sendMessage(this.state.account.id, encryptedBuffer, 'text/pgp-private-key'); }).catch((error) => { console.log('Error encrypting private key:', error); }); } processRemotePrivateKey(keyPair) { let regexp; let match; let public_key; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { public_key = match[0]; } if (public_key && this.state.keys && this.state.keys.public === public_key) { console.log('Private key is the same'); return; } this.setState({showImportPrivateKeyModal: true, privateKey: keyPair}); } async savePrivateKey(password) { utils.timestampedLog('Save encrypted private key'); password = password.trim(); let regexp; let match; let keyPair; let public_key; let encrypted_key; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = this.state.privateKey.match(regexp); if (match && match.length === 1) { public_key = match[0]; } if (public_key) { if (this.state.keys && this.state.keys.public === public_key) { this.setState({privateKeyImportStatus: 'Private key is the same', privateKeyImportSuccess: true}); return; } regexp = /(-----BEGIN PGP MESSAGE-----[^]*-----END PGP MESSAGE-----)/ig; match = this.state.privateKey.match(regexp); if (match && match.length === 1) { encrypted_key = match[0]; } if (encrypted_key) { await OpenPGP.decryptSymmetric(encrypted_key, password).then((privateKey) => { utils.timestampedLog('Decrypted PGP private pair'); this.setState({keyDifferentOnServer: false}) keyPair = public_key + "\n" + privateKey; this.processPrivateKey(keyPair); }).catch((error) => { this.setState({privateKeyImportStatus: 'No key received'}); console.log('Error decrypting PGP private key:', error); return }); } else { this.setState({privateKeyImportStatus: 'No encrypted key found'}); console.log('Error parsing PGP private key:', error); return } } else { await OpenPGP.decryptSymmetric(this.state.privateKey, password).then((keyPair) => { utils.timestampedLog('Decrypted PGP private pair'); this.setState({keyDifferentOnServer: false}) this.processPrivateKey(keyPair); }).catch((error) => { this.setState({privateKeyImportStatus: 'No key received'}); console.log('Error decrypting PGP private key:', error); return }); } } async processPrivateKey(keyPair) { utils.timestampedLog('Process key'); keyPair = keyPair.replace(/\r/g, '').trim(); let public_key; let private_key; let status; let keys = this.state.keys || {}; let regexp; let match; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { public_key = match[0]; } regexp = /(-----BEGIN PGP PRIVATE KEY BLOCK-----[^]*-----END PGP PRIVATE KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { private_key = match[0]; } if (public_key && private_key) { if (keys.private !== private_key && keys.public !== public_key) { let new_keys = {private: private_key, public: public_key} this.saveMyKey(new_keys); status = 'Private key copied successfully'; if (this.state.account) { this.state.account.sendMessage(this.state.accountId, 'Private key imported on another device', 'text/pgp-public-key-imported'); } this.requestSyncConversations(); } else { status = 'Private key is the same'; console.log(status); } this.setState({privateKeyImportStatus: status, privateKeyImportSuccess: true}); } else { this.setState({privateKeyImportStatus: 'Incorrect password!', privateKeyImportSuccess: false}); } } resetStartedByPush(from) { console.log('resetStartedByPush', from); this.startedByPush = false; if (this.state.lastSyncId) { this.requestSyncConversations(this.state.lastSyncId); } } requestSyncConversations(lastId=null) { console.log('Request sync conversations from', lastId); if (!this.state.account) { return; } if (!this.state.keys) { console.log('Wait for sync until we have keys') return; } if (this.startedByPush) { console.log('Wait for sync until incoming call ends') return; } if (this.syncRequested) { console.log('Sync already requested') return; } this.syncRequested = true; console.log('Request sync messages from server', lastId); this.state.account.syncConversations(lastId); } async savePublicKey(uri, key) { if (uri === this.state.accountId) { return; } if (!key) { console.log('Missing key'); return; } key = key.replace(/\r/g, '').trim(); if (!key.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the start of PGP public key'); return; } if (!key.endsWith("-----END PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the end of PGP public key'); return; } let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = {}; } if (myContacts[uri].publicKey === key) { console.log('Public key of', uri, 'did not change'); return; } utils.timestampedLog('Public key of', uri, 'saved'); this.saveSystemMessage(uri, 'Public key received', 'incoming'); myContacts[uri].publicKey = key; this.saveSylkContact(uri, myContacts[uri], 'savePublicKey'); this.sendPublicKey(uri); } async savePublicKeySync(uri, key) { console.log('Sync public key from', uri); if (!key) { console.log('Missing key'); return; } key = key.replace(/\r/g, '').trim(); if (!key.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the start of PGP public key'); return; } if (!key.endsWith("-----END PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the end of PGP public key'); return; } let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = {}; } if (myContacts[uri].publicKey === key) { console.log('Public key of', uri, 'did not change'); return; } console.log('Public key of', uri, 'saved'); myContacts[uri].publicKey = key; this.saveSylkContact(uri, myContacts[uri], 'savePublicKeySync'); } sendConferenceMessage(message) { if (!this.state.currentCall) { return; } if (!this.isConference(this.state.currentCall)) { return; } this.state.currentCall.sendMessage(message.text, 'text/plain'); message.direction = 'outgoing'; message.sent = true; message.received = true; this.saveConferenceMessage(this.state.currentCall.remoteIdentity.uri, message); } _sendMessage(uri, text, id, contentType, timestamp) { // Send outgoing messages if (this.state.account) { //console.log('Send', contentType, 'message', id, 'to', uri); let message = this.state.account.sendMessage(uri, text, contentType, {id: id, timestamp: timestamp}, (error) => { if (error) { console.log('Message', id, 'sending error:', error); this.outgoingMessageStateChanged(id, 'failed'); let status = error.toString(); if (status.indexOf('DNS lookup error') > -1) { status = 'Domain not found'; } this.renderSystemMessage(uri, status, 'incoming'); } }); //console.log(message); //message.on('stateChanged', (oldState, newState) => {this.outgoingMessageStateChanged(message.id, oldState, newState)}) } } textToGiftedMessage(text) { return { _id: uuid.v4(), text: text, createdAt: new Date(), received: false, direction: 'outgoing', user: {} }; } async sendMessage(uri, message) { message.sent = false; message.received = false; message.pending = true; message.direction = 'outgoing'; let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) === -1) { renderMessages[uri] = []; } let public_keys; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys) { public_keys = this.state.keys.public + "\n" + this.state.myContacts[uri].publicKey; } if (!message.contentType) { message.contentType = 'text/plain'; } if (message.contentType !== 'text/pgp-public-key' && public_keys && this.state.keys) { await OpenPGP.encrypt(message.text, public_keys).then((encryptedMessage) => { this._sendMessage(uri, encryptedMessage, message._id, message.contentType, message.createdAt); this.saveOutgoingMessage(uri, message, 1); }).catch((error) => { this.saveOutgoingMessage(uri, message, 2); console.log('Failed to encrypt message:', error); this.outgoingMessageStateChanged(message._id, 'failed'); }); } else { console.log('Outgoing non-encrypted message to', uri); this.saveOutgoingMessage(uri, message); this._sendMessage(uri, message.text, message._id, message.contentType, message.createdAt); } renderMessages[uri].push(message); if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = message.text.substring(0, 100); selectedContact.timestamp = message.createdAt; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact, messages: renderMessages}); } else { this.setState({messages: renderMessages}); } } async reSendMessage(message, uri) { await this.deleteMessage(message._id, uri).then((result) => { message._id = uuid.v4(); this.sendMessage(uri, message); }).catch((error) => { console.log('Failed to delete old messages'); }); } async saveOutgoingMessage(uri, message, encrypted=0) { this.saveOutgoingChatUri(uri, message.text); //console.log('saveOutgoingMessage', message.text); let ts = message.createdAt; let unix_timestamp = Math.floor(ts / 1000); let params = [this.state.accountId, message._id, JSON.stringify(ts), unix_timestamp, message.text, "text/plain", this.state.accountId, uri, "outgoing", "1", encrypted]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, encrypted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL insert message OK'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async saveConferenceMessage(room, message) { console.log('Save conference message', message._id); let messages = this.state.messages; let ts = message.createdAt; let unix_timestamp = Math.floor(ts / 1000); let contentType = "text/plain"; if (!message.direction) { message.direction = message.received ? 'incoming' : 'outgoing'; } let from_uri = message.direction === 'incoming' || message.received ? room : this.state.accountId; let to_uri = message.direction === 'incoming' || message.received ? this.state.accountId : room; let system = message.system ? '1' : null; let sender = !system ? message.user._id : null; var content = message.text; var params = [this.state.accountId, system, JSON.stringify(message.metadata), message.image, sender, message.local_url, message.url, message._id, JSON.stringify(ts), unix_timestamp, content, contentType, from_uri, to_uri, message.direction, 0, message.sent ? 1: 0, message.received ? 1: 0]; await this.ExecuteQuery("INSERT INTO messages (account, system, metadata, image, sender, local_url, url, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { console.log('SQL insert conference message', message._id); if (room in messages) { } else { messages[room] = [message]; } messages[room].push(message); this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async updateConferenceMessage(room, message, update=false) { console.log('Update conference message', message._id); let messages = this.state.messages; let sent = message.sent ? 1 : 0; let received = message.received ? 1 : 0; var params = [JSON.stringify(message.metadata), message.image, message.local_url, message.text, 0, sent, received, message._id]; await this.ExecuteQuery("update messages set metadata = ?, image = ?, local_url = ?, content = ?, pending = ?, sent = ?, received = ? where msg_id = ?", params).then((result) => { console.log('SQL update conference message', message._id); let renderMessages = messages[room]; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id === message._id) { msg.local_url = message.local_url; msg.image = message.image; msg.txt = message.text; msg.metadata = message.metadata; msg.pending = message.pending; msg.failed = message.failed; msg.sent = message.sent; msg.received = message.received; } newRenderMessages.push(msg); }); messages[room] = newRenderMessages; this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async deleteConferenceMessage(room, message) { console.log('Delete conference message', message._id); let messages = this.state.messages; var params = [message._id]; await this.ExecuteQuery("delete from messages where msg_id = ?", params).then((result) => { console.log('SQL delete conference message', message._id); let renderMessages = messages[room]; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id !== message._id) { newRenderMessages.push(msg); } }); messages[room] = newRenderMessages; this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } sql2GiftedChat(item, content) { let image; let timestamp = new Date(item.unix_timestamp * 1000); let failed = (item.received === 0) ? true: false; let received = item.received === 1 ? true : false; let sent = item.sent === 1 ? true : false; let pending = item.pending === 1 ? true : false; //console.log('sql2GiftedChat', failed); let msg; let from_uri = item.sender ? item.sender : item.from_uri; if (!item.image && item.local_url && utils.isImage(item.local_url)) { item.image = item.local_url; } if (!item.video && item.local_url && utils.isVideo(item.local_url)) { //item.video = item.local_url; } if (!item.audio && item.local_url && utils.isAudio(item.local_url)) { //item.audio = item.local_url; } let metadata = {}; if (item.metadata) { let metadata_obj = JSON.parse(item.metadata); Object.assign(metadata, metadata_obj); } msg = { _id: item.msg_id, key: item.msg_id, image: item.image, video: item.video, audio: item.audio, url: item.url, metadata: metadata, local_url: item.local_url, text: content || item.content || 'Empty message', encrypted: item.encrypted, createdAt: timestamp, sent: sent, direction: item.direction, received: received, pending: pending, system: item.system === 1 ? true : false, failed: failed, pinned: (item.pinned === 1) ? true: false, user: item.direction == 'incoming' ? {_id: from_uri, name: from_uri} : {} } //console.log(msg); return msg; } async outgoingMessageStateChanged(id, state) { let query; // mark message status // state can be failed or accepted utils.timestampedLog('Outgoing message', id, 'is', state); if (state === 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state === 'failed') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); if (query) { await this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } } async updateFileDownload(id, url, filePath) { let query = "UPDATE messages set local_url = ? where msg_id = ?"; //console.log(query); await this.ExecuteQuery(query[filePath, id]).then((results) => { console.log('URL', url, 'was downloaded to', filePath); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async messageStateChanged(id, state, data) { // valid API states: pending -> accepted -> delivered -> displayed, // error, failed or forbidden // valid UI render states: pending, read, received let reason = data.reason; let code = data.code; let failed = state === 'failed'; if (failed && code) { if (code > 500 || code === 408) { utils.timestampedLog('Message', id, 'failed on server:', reason, code); } } utils.timestampedLog('Message', id, 'is', state); let query; const failed_states = ['failed', 'error', 'forbidden']; if (state == 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state == 'delivered') { query = "UPDATE messages set pending = 0, sent = 1 where msg_id = '" + id + "'"; } else if (state == 'displayed') { query = "UPDATE messages set received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed_states.indexOf(state) > -1) { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else { console.log('Invalid message state', id, state); return; } //console.log(query); await this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, state, reason, code); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } messageStateChangedSync(obj) { // valid API states: pending -> accepted -> delivered -> displayed, // error, failed or forbidden // valid UI render states: pending, read, received let id = obj.messageId; let state = obj.state; //console.log('Sync message', id, 'state', state); let query; const failed_states = ['failed', 'error', 'forbidden']; if (state == 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state == 'delivered') { query = "UPDATE messages set pending = 0, sent = 1 where msg_id = '" + id + "'"; } else if (state == 'displayed') { query = "UPDATE messages set received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed_states.indexOf(state) > -1) { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); this.ExecuteQuery(query).then((results) => { //console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteMessage(id, uri, local=true) { utils.timestampedLog('Message', id, 'is deleted'); let query; // TODO send request to server //console.log(query); if (local) { this.addJournal(id, 'removeMessage', {uri: uri}); } this.removeFileFromMessage(id, uri); } async refetchMessages(days=1) { let timestamp = new Date(); let params; let unix_timestamp = Math.floor(timestamp / 1000); unix_timestamp = unix_timestamp - days * 24 * 3600; params = [this.state.accountId, unix_timestamp]; this.ExecuteQuery("select * from messages where account = ? and unix_timestamp < ? order by unix_timestamp desc limit 1", params).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); this.ExecuteQuery("delete from messages where account = ? and unix_timestamp > ?", params).then((results) => { console.log('SQL deleted', results.rowsAffected, 'messages'); this._notificationCenter.postSystemNotification(results.rowsAffected + ' messages removed'); console.log('Sync conversations since', item.msg_id, new Date(item.unix_timestamp * 1000)); this.setState({saveLastSyncId: item.msg_id}); setTimeout(() => { this.requestSyncConversations(item.msg_id); }, 100); }); } }).catch((error) => { console.log('SQL error:', error); }); } removeFileFromMessage(id, uri) { let query = "SELECT * from messages where msg_id = '" + id + "';"; this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); if (item.local_url) { RNFS.unlink(item.local_url).then((success) => { console.log('Removed file', item.local_url); }).catch((err) => { console.log('Error deleting file', item.local_url, err.message); }); } else if (item.url) { console.log('No local file available for', item.url); } query = "DELETE from messages where msg_id = '" + id + "'"; this.ExecuteQuery(query).then((results) => { this.deleteRenderMessage(id, uri); console.log('SQL deleted', results.rowsAffected, 'messages'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteMessageSync(id, uri) { //console.log('Sync message', id, 'is deleted'); let query; query = "DELETE from messages where msg_id = '" + id + "'"; this.ExecuteQuery(query).then((results) => { this.deleteRenderMessageSync(id, uri); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async expireMessage(id, duration=300) { utils.timestampedLog('Expire message', id, 'in', duration, 'seconds after read'); // TODO expire message } async deleteRenderMessage(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let myContacts = this.state.myContacts; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; if (uri in myContacts) { myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; if (existingMessages.length > 0 && existingMessages[0].id === id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } this.setState({messages: renderedMessages, myContacts: myContacts}); } } async deleteRenderMessageSync(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; this.setState({messages: renderedMessages}); } let idx = 'remove' + id; this.remove_sync_pending_item(idx); } async sendPendingMessage(uri, text, id, contentType, timestamp) { utils.timestampedLog('Outgoing pending message', id); if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys.public) { let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; await OpenPGP.encrypt(text, public_keys).then((encryptedMessage) => { //console.log('Outgoing encrypted message to', uri); this._sendMessage(uri, encryptedMessage, id, contentType, timestamp); }).catch((error) => { console.log('Failed to encrypt message:', error); this.outgoingMessageStateChanged(id, 'failed'); //this.saveSystemMessage(uri, 'Failed to encrypt message', 'outgoing'); }); } else { //console.log('Outgoing non-encrypted message to', uri); this._sendMessage(uri, text, id, contentType, timestamp); } } async sendPendingMessages() { if (this.mustLogout) { return; } let content; await this.ExecuteQuery("SELECT * from messages where pending = 1 and content_type like 'text/%' and from_uri = ?", [this.state.accountId]).then((results) => { let rows = results.rows; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); if (item.to_uri.indexOf('@conference.')) { continue; } if (item.to_uri.indexOf('@videoconference')) { continue; } content = item.content; let timestamp = new Date(item.unix_timestamp * 1000); this.sendPendingMessage(item.to_uri, content, item.msg_id, item.content_type, timestamp); } }).catch((error) => { console.log('SQL error:', error); }); await this.ExecuteQuery("SELECT * FROM messages where direction = 'incoming' and system is null and received = 0 and from_uri = ?", [this.state.accountId]).then((results) => { //console.log('SQL get messages OK'); let rows = results.rows; let imdn_msg; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); let timestamp = JSON.parse(item.timestamp, _parseSQLDate); imdn_msg = {id: item.msg_id, timestamp: timestamp, from_uri: item.from_uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { query = "UPDATE messages set received = 1 where msg_id = " + item.msg_id; //console.log(query); this.ExecuteQuery(query).then((results) => { }).catch((error) => { console.log('SQL error:', error); }); } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async updateRenderMessage(id, state, reason=null, code=null) { let query; let uri; let changes = false; //console.log('updateRenderMessage', id, state); query = "SELECT * from messages where msg_id = '" + id + "';"; //console.log(query); await this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); //console.log(item); uri = item.direction === 'outgoing' ? item.to_uri : item.from_uri; //console.log('Message uri', uri, 'new state', state); if (uri in this.state.messages) { let renderedMessages = this.state.messages; renderedMessages[uri].forEach((m) => { if (m._id === id) { if (state === 'accepted') { m.pending = false; changes = true; } if (state === 'delivered') { m.sent = true; m.pending = false; changes = true; } if (state === 'displayed') { m.received = true; m.sent = true; m.pending = false; changes = true; this.playMessageSound('outgoing'); } if (state === 'failed') { m.received = false; m.sent = false; m.pending = false; m.failed = true; changes = true; } if (state === 'pinned') { m.pinned = true; changes = true; } if (state === 'unpinned') { m.pinned = false; changes = true; } } }); if (changes) { this.setState({messages: renderedMessages}); if (state === 'failed') { reason = 'Message delivery failed: ' + reason; if (code) { reason = reason + '('+ code + ')'; } this.renderSystemMessage(uri, reason, 'incoming'); } } } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async saveOutgoingChatUri(uri, content='') { //console.log('saveOutgoingChatUri', uri); let query; let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } this.lookupPublicKey(myContacts[uri]); myContacts[uri].unread = []; if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); } if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } myContacts[uri].lastMessageId = null; myContacts[uri].lastCallDuration = null; myContacts[uri].timestamp = new Date(); myContacts[uri].direction = 'outgoing'; this.setState({myContacts: myContacts}); this.saveSylkContact(uri, myContacts[uri], 'saveOutgoingChatUri'); } pinMessage(id) { let query; query = "UPDATE messages set pinned = 1 where msg_id ='" + id + "'"; //console.log(query); this.ExecuteQuery(query).then((results) => { console.log('Message', id, 'pinned'); this.updateRenderMessage(id, 'pinned') this.addJournal(id, 'pinMessage'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } unpinMessage(id) { let query; query = "UPDATE messages set pinned = 0 where msg_id ='" + id + "'"; //console.log(query); this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, 'unpinned') this.addJournal(id, 'unPinMessage'); console.log('Message', id, 'unpinned'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async addJournal(id, action, data={}) { //console.log('Add journal entry:', action, id); this.mySyncJournal[uuid.v4()] = {id: id, action: action, data: data}; this.replayJournal(); } async replayJournal() { if (!this.state.account) { utils.timestampedLog('Sync journal later when going online...'); return; } if (this.mustLogout) { return; } let op; let executed_ops = []; Object.keys(this.mySyncJournal).forEach((key) => { if (this.mustLogout) { return; } executed_ops.push(key); op = this.mySyncJournal[key]; utils.timestampedLog('Sync journal', op.action, op.id); if (op.action === 'removeConversation') { this.state.account.removeConversation(op.id, (error) => { // TODO: add period and delete remote flags if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation was completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'readConversation') { this.state.account.markConversationRead(op.id, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'removeMessage') { this.state.account.removeMessage({id: op.id, receiver: op.data.uri}, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } }); executed_ops.forEach((key) => { delete this.mySyncJournal[key]; }); storage.set('mySyncJournal', this.mySyncJournal); this.sendPendingMessages(); } async confirmRead(uri){ if (uri.indexOf('@') === -1) { return; } if (uri.indexOf('@conference.') > -1) { return; } if (uri.indexOf('@videoconference.') > -1) { return; } if (uri in this.state.decryptingMessages) { return; } //console.log('Confirm read messages for', uri); let displayed = []; await this.ExecuteQuery("SELECT * FROM messages where from_uri = '" + uri + "' and received = 1 and encrypted not in (1, 3) and system is NULL and to_uri = ?", [this.state.accountId]).then((results) => { let rows = results.rows; if (rows.length > 0) { //console.log('We must confirm read of', rows.length, 'messages'); } for (let i = 0; i < rows.length; i++) { var item = rows.item(i); if (this.sendDispositionNotification(item)) { displayed.push(item.msg_id); } } if (displayed.length > 0) { let sql_ids = ''; let i = 1; displayed.forEach((msg_id) => { sql_ids = sql_ids + "'" + msg_id + "'"; if (i < displayed.length) { sql_ids = sql_ids + ', '; } i = i + 1; }); let query = "UPDATE messages set received = 2 where msg_id in (" + sql_ids + ")"; //console.log(query); this.ExecuteQuery(query).then((results) => { //console.log('Sent disposition saved for', displayed.length, 'messages'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } }).catch((error) => { console.log('SQL error:', error); }); this.resetUnreadCount(uri); } async resetUnreadCount(uri) { //console.log('--- resetUnreadCount', uri); let myContacts = this.state.myContacts; let missedCalls = this.state.missedCalls; let idx; let changes = false; if (uri in myContacts) { } else { return; } if (myContacts[uri].unread.length > 0) { myContacts[uri].unread = []; myContacts[uri].unread.forEach((id) => { idx = missedCalls.indexOf(id); if (idx > -1) { missedCalls.splice(idx, 1); } }); changes = true; } if (myContacts[uri].lastCallId) { idx = missedCalls.indexOf(myContacts[uri].lastCallId); if (idx > -1) { missedCalls.splice(idx, 1); } } idx = myContacts[uri].tags.indexOf('missed'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); changes = true; } this.updateTotalUread(myContacts); if (changes) { this.saveSylkContact(uri, myContacts[uri], 'resetUnreadCount'); this.addJournal(uri, 'readConversation'); } this.setState({missedCalls: missedCalls}); } async sendDispositionNotification(message, state='displayed') { if (!this.state.account) { return false; } let query; let result = {}; let id = message.msg_id || message.id; let uri = message.sender ? message.sender.uri : message.from_uri; this.state.account.sendDispositionNotification(uri, id, message.timestamp, state,(error) => { if (!error) { utils.timestampedLog('Message', id, 'was', state, 'now'); return true; } else { utils.timestampedLog(state, 'notification for message', id, 'send failed:', error); return false; } }); return false; } loadEarlierMessages() { if (!this.state.selectedContact) { return; } let myContacts = this.state.myContacts; let uri = this.state.selectedContact.uri; let limit = this.state.messageLimit * this.state.messageZoomFactor; if (myContacts[uri].totalMessages < limit) { //console.log('No more messages for', uri); return; } let messageZoomFactor = this.state.messageZoomFactor; messageZoomFactor = messageZoomFactor + 1; this.setState({messageZoomFactor: messageZoomFactor}); setTimeout(() => { this.getMessages(this.state.selectedContact.uri); }, 10); } async decryptMessage(message, updateContact=false) { // encrypted // 0 not encrypted // null not encrypted // 1 encrypted content // 2 decrypted content // 3 failed to decrypt if (!this.state.keys.private) { return; } let id = message.msg_id; let decryptingMessages = this.state.decryptingMessages; let msg; let pending_messages = []; let idx; await OpenPGP.decrypt(message.content, this.state.keys.private).then((content) => { //console.log('Message', id, message.content_type, 'to', message.to_uri, 'was decrypted'); let messages = this.state.messages; let uri = message.direction === 'incoming' ? message.from_uri : message.to_uri; if (uri in decryptingMessages) { pending_messages = decryptingMessages[uri]; idx = pending_messages.indexOf(id); if (pending_messages.length > 10) { let status = 'Decrypting ' + pending_messages.length + ' messages with'; this._notificationCenter.postSystemNotification(status, {body: uri}); } else if (pending_messages.length === 10) { let status = 'All messages decrypted'; this._notificationCenter.postSystemNotification(status); } if (idx > -1) { pending_messages.splice(idx, 1); decryptingMessages[uri] = pending_messages; this.setState({decryptingMessages: decryptingMessages}); } } if (updateContact) { let myContacts = this.state.myContacts; console.log('Update contact after decryption', uri); if (this.mustPlayIncomingSoundAfterSync) { this.playMessageSound(); this.mustPlayIncomingSoundAfterSync = false; } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; myContacts[uri].timestamp = message.timestamp; this.saveSylkContact(uri, myContacts[uri], 'decryptMessage'); this.setState({myContacts: myContacts}); } } if (uri in messages) { let render_messages = messages[uri]; if (message.content_type === 'text/html') { content = utils.html2text(content); } else if (message.content_type === 'text/plain') { content = content; } else if (message.content_type.indexOf('image/') > -1) { image = `data:${message.content_type};base64,${btoa(content)}` } msg = this.sql2GiftedChat(message, content); render_messages.push(msg); messages[uri] = render_messages; if (pending_messages.length === 0) { this.confirmRead(uri); this.setState({message: messages}); } } let params = [content, id]; this.ExecuteQuery("update messages set encrypted = 2, content = ? where msg_id = ?", params).then((result) => { //console.log('SQL updated message decrypted', id); }).catch((error) => { console.log('SQL message update error:', error); }); }).catch((error) => { let params = [id]; this.ExecuteQuery("update messages set encrypted = 3 where msg_id = ?", params).then((result) => { console.log('SQL failed to decrypt message', id, 'rows affected', result.rowsAffected); }).catch((error) => { console.log('SQL message update error:', error); }); }); } lookupPublicKey(contact) { if (contact.uri.indexOf('@guest') > -1) { return; } if (contact.uri.indexOf('anonymous') > -1) { return; } if (!contact.publicKey && !contact.conference && this.state.connection) { this.state.connection.lookupPublicKey(contact.uri); } } async contactsCount() { let query = "SELECT count(*) as rows FROM contacts where account = ?"; let rows; let total; await this.ExecuteQuery(query, [this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'total contacts'); }).catch((error) => { console.log('SQL error:', error); }); } async getMessages(uri, pinned=false) { if (this.state.syncConversations) { return; } let messages = this.state.messages; let myContacts = this.state.myContacts; let msg; let query; let rows = 0; let total = 0; let last_messages = []; let orig_uri; if (!uri) { query = "SELECT count(*) as rows FROM messages where (from_uri = ? and direction = 'outgoing') or (to_uri = ? and direction = 'incoming')"; await this.ExecuteQuery(query, [this.state.accountId, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'total messages'); }).catch((error) => { console.log('SQL error:', error); }); return; } orig_uri = uri; if (Object.keys(myContacts).indexOf(uri) === -1) { this.setState({messages: {}}); return; } if (utils.isPhoneNumber(uri) && uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } else { this.resetUnreadCount(orig_uri); this.lookupPublicKey(myContacts[orig_uri]); } console.log('Get messages with', uri, 'with zoom factor', this.state.messageZoomFactor); let limit = this.state.messageLimit * this.state.messageZoomFactor; query = "SELECT count(*) as rows FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?))"; if (pinned) { query = query + ' and pinned = 1'; } await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log('Got', total, 'messages with', uri, 'from database'); }).catch((error) => { console.log('SQL error:', error); }); if (uri in myContacts) { myContacts[uri].totalMessages = total; } query = "SELECT * FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?)) "; if (pinned) { query = query + ' and pinned = 1'; } query = query + ' order by unix_timestamp desc limit ?, ?'; await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId, this.state.messageStart, limit]).then((results) => { //console.log('SQL get messages, rows =', results.rows.length); let rows = results.rows; messages[orig_uri] = []; let content; let ts; let last_message; let last_message_id; let last_direction; let messages_to_decrypt = []; let decryptingMessages = {}; let msg; let enc; let last_content = null; for (let i = 0; i < rows.length; i++) { var item = rows.item(i); if (false) { //console.log('Remove broken message', item); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } content = item.content; if (!content && !item.image) { content = 'Empty message...'; } last_direction = item.direction; let timestamp; last_message = null; last_message_id = null; let unix_timestamp; if (item.unix_timestamp === 0) { timestamp = JSON.parse(item.timestamp, _parseSQLDate); unix_timestamp = Math.floor(timestamp / 1000); item.unix_timestamp = unix_timestamp; this.ExecuteQuery('update messages set unix_timestamp = ? where msg_id = ?', [unix_timestamp, item.msg_id]); } else { timestamp = new Date(item.unix_timestamp * 1000); } const is_encrypted = content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && content.indexOf('-----END PGP MESSAGE-----') > -1; if (is_encrypted) { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; if (item.encrypted === null) { item.encrypted = 1; } /* encrypted: 1 = unencrypted 2 = decrypted 3 = failed to decrypt message */ enc = parseInt(item.encrypted); if (enc && enc !== 3 ) { if (uri in decryptingMessages) { } else { decryptingMessages[orig_uri] = []; } decryptingMessages[orig_uri].push(item.msg_id); messages_to_decrypt.push(item); } } else { if (item.content_type === 'text/html') { content = utils.html2text(content); } else if (item.content_type === 'text/plain') { content = content; } else if (item.content_type.indexOf('image/') > -1) { image = `data:${item.content_type};base64,${btoa(content)}` } else if (item.content_type === 'application/sylk-contact-update') { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; console.log('Remove update contact message', item.id); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); continue; } else { console.log('Unknown message', item.msg_id, 'type', item.content_type); myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } //console.log(item); if (last_content === content) { continue; } last_content = content; msg = this.sql2GiftedChat(item, content); messages[orig_uri].push(msg); } } console.log('Got', messages[orig_uri].length, 'out of', total, 'messages for', uri); last_messages = messages[orig_uri]; last_messages.reverse(); if (last_messages.length > 0) { last_messages.forEach((last_item) => { if (!last_item.image && !last_item.system && last_item.text) { last_message = last_item.text.substring(0, 100); last_message_id = last_item.id; } else { return; } }); } if (orig_uri in myContacts) { if (last_message && last_message != myContacts[orig_uri].lastMessage) { myContacts[orig_uri].lastMessage = last_message; myContacts[orig_uri].lastMessageId = last_message_id; this.saveSylkContact(uri, myContacts[orig_uri], 'getMessages'); this.setState({myContacts: myContacts}); } } this.setState({messages: messages, decryptingMessages: decryptingMessages}); let i = 1; messages_to_decrypt.forEach((item) => { var updateContact = messages_to_decrypt.length === i; //console.log('To decrypt', messages_to_decrypt.length, 'updateContact =', updateContact); this.decryptMessage(item, updateContact); i = i + 1; }); }).catch((error) => { console.log('SQL error:', error); }); } async deleteMessages(uri, local=true) { console.log('Delete messages for', uri); let myContacts = this.state.myContacts; let messages = this.state.messages; let query; let params; let orig_uri = uri; if (uri) { if (uri.indexOf('@') === -1 && utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } else { if (local) { this.addJournal(orig_uri, 'removeConversation'); } } } if (uri) { let dir = RNFS.DocumentDirectoryPath + '/conference/' + uri + '/files'; RNFS.unlink(dir).then((success) => { console.log('Removed folder', dir); }).catch((err) => { //console.log('Error deleting folder', dir, err.message); }); query = "DELETE FROM messages where ((from_uri = ? and to_uri = ? and direction = 'incoming') or (from_uri = ? and to_uri = ? and direction = 'outgoing'))"; params = [uri, this.state.accountId, this.state.accountId, uri]; } else { console.log('--- Wiping device --- '); let dir = RNFS.DocumentDirectoryPath + '/conference/'; RNFS.unlink(dir).then((success) => { console.log('Removed folder', dir); }).catch((err) => { //console.log('Error deleting folder', dir, err.message); }); query = "DELETE FROM messages where (account = ? and to_uri = ? and direction = 'incoming') or (account = ? and from_uri = ? and direction = 'outgoing')"; params = [this.state.accountId, this.state.accountId, this.state.accountId, this.state.accountId]; this.setState({messages: {}}); this.saveLastSyncId(null); } await this.ExecuteQuery(query, params).then((result) => { if (result.rowsAffected) { console.log('SQL deleted', result.rowsAffected, 'messages'); if (uri) { this._notificationCenter.postSystemNotification(result.rowsAffected + ' messages removed'); } } if (!uri) { this.deleteAllContacts(this.state.accountId); } else { if (result.rowsAffected === 0) { this.removeContact(orig_uri); } else { if (orig_uri in messages) { delete messages[orig_uri]; this.setState({messages: messages}); } if (orig_uri in myContacts) { myContacts[orig_uri].totalMessages = 0; myContacts[orig_uri].lastMessage = null; myContacts[orig_uri].lastMessageId = null; this.setState({myContacts: myContacts}); } } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteAllContacts(account) { let query = 'delete from contacts where account = ?'; this.setState({myContacts: {}}); await this.ExecuteQuery(query, [account]).then((result) => { if (result.rowsAffected) { console.log('SQL deleted', result.rowsAffected, 'contacts'); } this.deleteKeys(account); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteKeys(account) { let query = 'delete from keys where account = ?'; this.setState({keys: null}); await this.ExecuteQuery(query, [account]).then((result) => { if (result.rowsAffected) { console.log('SQL deleted', result.rowsAffected, 'keys'); } setTimeout(() => { this.logout(); }, 1000); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } playMessageSound(direction='incoming') { let must_play_sound = true; if (direction === 'incoming') { if (this.incoming_sound_ts) { let diff = (Date.now() - this.incoming_sound_ts)/ 1000; if (diff < 5) { must_play_sound = false; } } } else { if (this.outgoing_sound_ts) { let diff = (Date.now() - this.outgoing_sound_ts)/ 1000; if (diff < 5) { must_play_sound = false; } } } if (!must_play_sound) { console.log('Play incoming sound skipped'); } console.log('Play', direction, 'message sound in the', this.state.appState); if (Platform.OS === 'android' && this.state.appState === 'foreground') { // } try { if (Platform.OS === 'ios') { SoundPlayer.setSpeaker(true); } //SoundPlayer.playSoundFile('message_received', 'wav'); if (direction === 'incoming') { this.incoming_sound_ts = Date.now(); SoundPlayer.playSoundFile('beluga_in', 'wav'); } else { this.outgoing_sound_ts = Date.now(); SoundPlayer.playSoundFile('beluga_out', 'wav'); } } catch (e) { console.log('Error playing', direction,' sound:', e); } } async removeMessage(message, uri=null) { if (uri === null) { uri = message.sender.uri; } await this.deleteMessage(message.id, uri, false).then((result) => { console.log('Message', message.id, 'to', uri, 'is removed'); }).catch((error) => { //console.log('Failed to remove message', message.id, 'to', uri); return; }); let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) === -1) { return; } let existingMessages = renderMessages[uri]; let newMessages = []; existingMessages.forEach((msg) => { if (msg.id === message.id) { return; } newMessages.push(msg); }); let myContacts = this.state.myContacts; if (uri in myContacts) { if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; } let idx = myContacts[uri].unread.indexOf(message.id); if (idx > -1) { myContacts[uri].unread.splice(idx, 1); } if (myContacts[uri].lastMessageId === message.id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } renderMessages[uri] = newMessages; this.setState({messages: renderMessages, myContacts: myContacts}); } async removeConversation(obj) { let uri = obj; //console.log('removeConversation', uri); let renderMessages = this.state.messages; await this.deleteMessages(uri, false).then((result) => { utils.timestampedLog('Conversation with', uri, 'was removed'); }).catch((error) => { console.log('Failed to delete conversation with', uri); }); } removeConversationSync(obj) { let uri = obj.content; //console.log('Sync remove conversation with', uri, 'before', obj.timestamp); let query; let unix_timestamp = Math.floor(obj.timestamp / 1000); query = "DELETE FROM messages where (from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?) and (unix_timestamp < ? or unix_timestamp = 0)"; this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId, unix_timestamp]).then((result) => { if (result.rowsAffected > 0) { console.log('SQL deleted', result.rowsAffected, 'messages with', uri, 'before', obj.timestamp); } }).catch((error) => { console.log('SQL delete conversation sync error:', error); }); let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].timestamp < obj.timestamp) { this.deleteSylkContact(uri); } } async readConversation(obj) { let uri = obj; this.resetUnreadCount(uri) } removeContact(uri) { let myContacts = this.state.myContacts; this.deleteSylkContact(uri); if (this.state.selectedContact && this.state.selectedContact.uri === uri) { this.setState({selectedContact: null}); } let renderMessages = this.state.messages; if (uri in renderMessages) { delete renderMessages[uri]; this.setState({messages: renderMessages}); } } add_sync_pending_item(item) { if (this.sync_pending_items.indexOf(item) > -1) { return; } this.sync_pending_items.push(item); if (this.sync_pending_items.length == 1) { //console.log('Sync started ---'); this.setState({syncConversations: true}); if (this.syncTimer === null) { this.syncTimer = setTimeout(() => { this.resetSyncTimer(); }, 1000 * 60); } } } resetSyncTimer() { if (this.sync_pending_items.length > 0) { this.sync_pending_items = []; console.log('Sync ended by timer ---'); //console.log('Pending tasks:', this.sync_pending_items); this.afterSyncTasks(); } } remove_sync_pending_item(item) { //console.log('remove_sync_pending_item', this.sync_pending_items.length); let idx = this.sync_pending_items.indexOf(item); if (idx > -1) { this.sync_pending_items.splice(idx, 1); } if (this.sync_pending_items.length == 0 && this.state.syncConversations) { if (this.syncTimer !== null) { clearTimeout(this.syncTimer); this.syncTimer = null; } this.afterSyncTasks(); } else { if (this.sync_pending_items.length > 10 && this.sync_pending_items.length % 10 == 0) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } else if (this.sync_pending_items.length > 0 && this.sync_pending_items.length < 10) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } } } async insertPendingMessages() { let query = "INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES " if (this.pendingNewSQLMessages.length > 0) { //console.log('Inserting', this.pendingNewSQLMessages.length, 'new messages'); } let pendingNewSQLMessages = this.pendingNewSQLMessages; this.pendingNewSQLMessages = []; let all_values = []; let n = 0; let i = 1; if (pendingNewSQLMessages.length > 0) { pendingNewSQLMessages.forEach((values) => { Array.prototype.push.apply(all_values, values); query = query + "("; n = 0; while (n < values.length ) { query = query + "?" if (n < values.length - 1) { query = query + ","; } n = n + 1; } query = query + ")"; if (pendingNewSQLMessages.length > i) { query = query + ", "; } i = i + 1; }); this.ExecuteQuery(query, all_values).then((result) => { //console.log('SQL inserted', pendingNewSQLMessages.length, 'messages'); this.newSyncMessagesCount = this.newSyncMessagesCount + pendingNewSQLMessages.length; }).catch((error) => { console.log('SQL error inserting bulk messages:', error.message); console.log('query:', query); pendingNewSQLMessages.forEach((values) => { this.ExecuteQuery(query, values).then((result) => { this.newSyncMessagesCount = this.newSyncMessagesCount + 1; }).catch((error) => { if (error.message.indexOf('SQLITE_CONSTRAINT_PRIMARYKEY') > -1) { console.log('Duplicate message id', values[2]); } else { console.log('SQL error inserting message', values[2], error.message); } }); }); }); } } async afterSyncTasks() { this.insertPendingMessages(); if (this.newSyncMessagesCount) { console.log('Synced', this.newSyncMessagesCount, 'messages from server'); this.newSyncMessagesCount = 0; } this.setState({syncConversations: false}); this.sync_pending_items = []; let myContacts = this.state.myContacts; let updateContactUris = this.state.updateContactUris; let replicateContacts = this.state.replicateContacts; let deletedContacts = this.state.deletedContacts; //console.log('updateContactUris:', Object.keys(updateContactUris).toString()); //console.log('replicateContacts:', Object.keys(replicateContacts).toString()); //console.log('deletedContacts:', Object.keys(deletedContacts).toString()); let uris = Object.keys(replicateContacts).concat(Object.keys(updateContactUris)); uris = [... new Set(uris)]; //console.log('Update contacts:', uris.toString()); // sync changed myContacts with SQL database let created; let old_tags; uris.forEach((uri) => { if (uri in myContacts) { created = false; } else { if (uri in deletedContacts) { return } myContacts[uri] = this.newContact(uri); created = true; } if (uri in replicateContacts) { myContacts[uri].name = replicateContacts[uri].name; myContacts[uri].email = replicateContacts[uri].email; myContacts[uri].organization = replicateContacts[uri].organization; old_tags = myContacts[uri].tags; myContacts[uri].tags = replicateContacts[uri].tags; myContacts[uri].participants = replicateContacts[uri].participants; if (myContacts[uri].timestamp > replicateContacts[uri].timestamp) { if (old_tags.indexOf('missed') > -1 && replicateContacts[uri].tags.indexOf('missed') === -1) { myContacts[uri].tags.push('missed'); } } if (old_tags.indexOf('chat') > -1 && replicateContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (old_tags.indexOf('history') > -1 && replicateContacts[uri].tags.indexOf('history') === -1) { myContacts[uri].tags.push('history'); } if (replicateContacts[uri].timestamp > myContacts[uri].timestamp || created) { myContacts[uri].timestamp = replicateContacts[uri].timestamp; if (uri === this.state.accountId) { let name = replicateContacts[uri].name || ''; let organization = replicateContacts[uri].organization || ''; this.setState({displayName: name, organization: organization, email: myContacts[uri].email}); } } } if (uri in updateContactUris && updateContactUris[uri] > myContacts[uri].timestamp) { myContacts[uri].timestamp = updateContactUris[uri]; } this.saveSylkContact(uri, myContacts[uri], 'syncEnd'); }); let purgeMessages = this.state.purgeMessages; purgeMessages.forEach((id) => { this.deleteMessage(id, this.state.accountId); }); Object.keys(deletedContacts).forEach((uri) => { this.removeConversationSync(deletedContacts[uri]) }); this.setState({purgeMessages:[], syncConversations: false, firstSyncDone: true, updateContactUris: {}, replicateContacts: {}, deletedContacts: {}}); if (this.syncStartTimestamp) { let diff = (Date.now() - this.syncStartTimestamp)/ 1000; this.syncStartTimestamp = null; console.log('Sync ended after', diff, 'seconds'); } setTimeout(() => { this.addTestContacts(); this.refreshNavigationItems(); this.updateServerHistory('syncConversations') if (this.state.selectedContact) { this.getMessages(this.state.selectedContact.uri); } }, 1000); } async syncConversations(messages) { if (this.sync_pending_items.length > 0) { console.log('Sync already in progress'); return; } if (this.mustLogout || this.currentRoute === '/logout') { return; } if (this.currentRoute === '/login') { return; } this.syncStartTimestamp = new Date(); let myContacts = this.state.myContacts; let renderMessages = this.state.messages; if (messages.length > 0) { console.log('Sync', messages.length, 'events from server'); //this._notificationCenter.postSystemNotification('Syncing messages with the server'); this.add_sync_pending_item('sync_in_progress'); } else { this.setState({firstSyncDone: true}); console.log('Sync ended'); setTimeout(() => { this.addTestContacts(); this.refreshNavigationItems(); this.updateServerHistory('syncConversations') }, 500); } let i = 0; let idx; let uri; let last_id; let content; let contact; let existingMessages; let formatted_date; let newMessages = []; let lastMessages = {}; let updateContactUris = {}; let deletedContacts = {}; let last_timestamp; let stats = {state: 0, remove: 0, incoming: 0, outgoing: 0, delete: 0, read: 0} let purgeMessages = this.state.purgeMessages; messages.forEach((message) => { if (this.mustLogout) { return; } last_timestamp = message.timestamp; i = i + 1; uri = null; if (message.contentType === 'application/sylk-message-remove') { uri = message.content.contact; } else if (message.contentType === 'application/sylk-conversation-remove') { uri = message.content; } else if (message.contentType === 'application/sylk-conversation-read' ) { uri = message.content; } else if (message.contentType === 'message/imdn') { } else { if (message.sender.uri === this.state.account.id) { uri = message.receiver; } else { uri = message.sender.uri; } } if (uri) { //console.log('Process journal', i, 'of', messages.length, message.contentType, uri, message.timestamp); } let d = new Date(2019); if (message.timestamp < d) { console.log('Skip broken journal with broken date', message.id); purgeMessages.push(message.id); return; } if (!message.content) { console.log('Skip broken journal with empty body', message.id); purgeMessages.push(message.id); return; } if (message.contentType !== 'application/sylk-conversation-remove' && message.contentType !== 'application/sylk-message-remove' && uri && Object.keys(myContacts).indexOf(uri) === -1) { if (uri.indexOf('@') > -1 && !utils.isEmailAddress(uri)) { return; } console.log('Will add a new contact', uri); myContacts[uri] = this.newContact(uri); myContacts[uri].timestamp = message.timestamp; //this.setState({myContacts: myContacts}); } //console.log('Sync', message.timestamp, message.contentType, uri); if (message.contentType === 'application/sylk-message-remove') { idx = 'remove' + message.id; this.add_sync_pending_item(idx); this.deleteMessageSync(message.id, uri); if (uri in renderMessages) { existingMessages = renderMessages[uri]; newMessages = []; existingMessages.forEach((msg) => { if (msg.id === message.id) { return; } newMessages.push(msg); }); renderMessages[uri] = newMessages; } if (uri in myContacts) { let idx = myContacts[uri].unread.indexOf(message.id); if (idx > -1) { myContacts[uri].unread.splice(idx, 1); } if (myContacts[uri].lastMessageId === message.id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } if (uri in lastMessages && lastMessages[uri] === message.id) { delete lastMessages[uri]; } stats.delete = stats.delete + 1; } else if (message.contentType === 'application/sylk-conversation-remove') { if (uri in myContacts && message.timestamp > myContacts[uri].timestamp) { delete myContacts[uri]; } if (uri in updateContactUris) { delete updateContactUris[uri]; } if (uri in lastMessages) { delete lastMessages[uri]; } if (uri in renderMessages) { delete renderMessages[uri]; } deletedContacts[uri] = message; stats.remove = stats.remove + 1; } else if (message.contentType === 'application/sylk-conversation-read') { updateContactUris[uri] = last_timestamp; myContacts[uri].unread = []; stats.read = stats.read + 1; } else if (message.contentType === 'message/imdn') { this.messageStateChangedSync({messageId: message.id, state: message.state}); stats.state = stats.state + 1; } else { this.add_sync_pending_item(message.id); if (message.sender.uri === this.state.account.id) { if (message.contentType !== 'application/sylk-contact-update') { if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } myContacts[uri].lastMessageId = message.id; myContacts[uri].lastMessage = null; // need to be loaded later after decryption myContacts[uri].lastCallDuration = null; myContacts[uri].direction = 'outgoing'; if (myContacts[uri].tags.indexOf('chat') === -1 && (message.contentType === 'text/plain' || message.contentType === 'text/html')) { myContacts[uri].tags.push('chat'); } lastMessages[uri] = message.id; if (message.timestamp > myContacts[uri].timestamp) { updateContactUris[uri] = message.timestamp; myContacts[uri].timestamp = message.timestamp; } } stats.outgoing = stats.outgoing + 1; this.outgoingMessageSync(message); } else { if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } if (message.timestamp > myContacts[uri].timestamp) { updateContactUris[uri] = message.timestamp; myContacts[uri].timestamp = message.timestamp; } myContacts[uri].lastMessageId = message.id; myContacts[uri].lastMessage = null; // need to be loaded later after decryption myContacts[uri].lastCallDuration = null; myContacts[uri].direction = 'incoming'; if (this.state.selectedContact && this.state.selectedContact.uri === uri) { this.mustPlayIncomingSoundAfterSync = true; } if (myContacts[uri].tags.indexOf('chat') === -1 && (message.contentType === 'text/plain' || message.contentType === 'text/html')) { myContacts[uri].tags.push('chat'); } lastMessages[uri] = message.id; if (message.dispositionNotification.indexOf('display') > -1) { myContacts[uri].unread.push(message.id); } stats.incoming = stats.incoming + 1; this.incomingMessageSync(message); } } last_id = message.id; }); this.updateTotalUread(myContacts); /* if (messages.length > 0) { Object.keys(stats).forEach((key) => { console.log('Sync', stats[key], key); }); } */ this.setState({messages: renderMessages, updateContactUris: updateContactUris, deletedContacts: deletedContacts, purgeMessages: purgeMessages }); this.remove_sync_pending_item('sync_in_progress'); Object.keys(lastMessages).forEach((uri) => { //console.log('Update last message for', uri); // TODO update lastMessage content for each contact }); if (last_id) { this.saveLastSyncId(last_id, true); } } async publicKeyReceived(message) { if (message.publicKey) { this.savePublicKey(message.uri, message.publicKey.trim()); } else { console.log('No public key available on server for', message.uri); if (message.uri === this.state.accountId) { var uri = uuid.v4() + '@' + this.state.defaultDomain; //console.log('Send 1st public to', uri); this.sendPublicKey(uri); } } } async incomingMessage(message) { console.log('Message', message.id, message.contentType, 'was received'); // Handle incoming messages this.saveLastSyncId(message.id); if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'application/sylk-contact-update') { return; } if (message.contentType === 'text/pgp-public-key') { this.savePublicKey(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-private-key' && message.sender.uri === this.state.account.id) { console.log('Received PGP private key from another device'); this.processRemotePrivateKey(message.content); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; if (is_encrypted) { if (!this.state.keys || !this.state.keys.private) { console.log('Missing private key, cannot decrypt message'); this.sendDispositionNotification(message, 'error'); this.saveSystemMessage(message.sender.uri, 'Cannot decrypt message, no private key', 'incoming'); } else { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Incoming message', message.id, 'decrypted'); this.handleIncomingMessage(message, decryptedBody); }).catch((error) => { console.log('Failed to decrypt message', message.id, error); this.sendPublicKey(message.sender.uri); this.sendDispositionNotification(message, 'error'); this.saveSystemMessage(message.sender.uri, 'Cannot decrypt last message, wrong key', 'incoming'); }); } } else { //console.log('Incoming message is not encrypted'); this.handleIncomingMessage(message); } } handleIncomingMessage(message, decryptedBody=null) { let content = decryptedBody || message.content; if (!this.state.selectedContact || this.state.selectedContact.uri != message.sender.uri) { this.postAndroidMessageNotification(message.sender.uri, content); } this.saveIncomingMessage(message, decryptedBody); let renderMessages = this.state.messages; if (this.state.selectedContact) { if (message.sender.uri === this.state.selectedContact.uri) { if (message.sender.uri in renderMessages) { if (renderMessages[message.sender.uri].some((obj) => obj.id === message.id)) { return; } } else { renderMessages[message.sender.uri] = []; } } renderMessages[message.sender.uri].push(utils.sylkToRenderMessage(message, decryptedBody, 'incoming')); let selectedContact = this.state.selectedContact; selectedContact.lastMessage = content.substring(0, 100); selectedContact.timestamp = message.timestamp; selectedContact.direction = 'incoming'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact, messages: renderMessages}); } else { this.setState({messages: renderMessages}); } if (this.state.selectedContact || this.currentRoute === '/ready') { this.playMessageSound(); } this.notifyIncomingMessageWhileInACall(message.sender.uri); } async incomingMessageSync(message) { //console.log('Sync incoming message', message); // Handle incoming messages if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); this.savePublicKeySync(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-public-key-imported') { return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { this.saveIncomingMessageSync(message, null, true); } else { //console.log('Incoming message', message.id, 'not encrypted from', message.sender.uri); this.saveIncomingMessageSync(message); } this.remove_sync_pending_item(message.id); } async outgoingMessage(message) { console.log('Outgoing message', message.contentType, message.id, 'to', message.receiver); this.saveLastSyncId(message.id); if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'text/pgp-public-key') { return; } if (message.sender.uri.indexOf('@conference') > -1) { return; } if (message.sender.uri.indexOf('@videoconference') > -1) { return; } if (message.contentType === 'text/pgp-public-key-imported') { this.hideExportPrivateKeyModal(); this.hideImportPrivateKeyModal(); return; } if (message.contentType === 'message/imdn') { return; } if (message.contentType === 'text/pgp-private-key' && message.sender.uri === this.state.account.id) { console.log('Received my own PGP private key'); this.processRemotePrivateKey(message.content); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Outgoing message', message.id, 'decrypted to', message.receiver, message.contentType); content = decryptedBody; if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContact(content); } else { this.saveOutgoingMessageSql(message, content, 1); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = myContacts[uri].lastMessage; selectedContact.timestamp = message.timestamp; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact}); } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > -1) { if (!renderMessages[uri].some((obj) => obj.id === message.id)) { renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); this.setState({renderMessages: renderMessages}); } else { return; } } } this.setState({myContacts: myContacts}); this.saveSylkContact(uri, myContacts[uri], 'outgoingMessage'); } }).catch((error) => { console.log('Failed to decrypt my own message in outgoingMessage:', error); return; }); } else { if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContact(content); } else { this.saveOutgoingMessageSql(message); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > -1) { if (!renderMessages[uri].some((obj) => obj.id === message.id)) { renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); this.setState({renderMessages: renderMessages}); } else { return; } } this.saveSylkContact(uri, myContacts[uri], 'outgoingMessage'); } } } async outgoingMessageSync(message) { //console.log('Sync outgoing message', message.id, 'to', message.receiver); if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'message/imdn') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { if (message.contentType === 'application/sylk-contact-update') { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Sync outgoing message', message.id, message.contentType, 'decrypted to', message.receiver); this.handleReplicateContactSync(decryptedBody, message.id, message.timestamp); this.remove_sync_pending_item(message.id); }).catch((error) => { console.log('Failed to decrypt my own message in sync:', error); this.remove_sync_pending_item(message.id); return; }); } else { this.saveOutgoingMessageSqlBatch(message, null, true); this.remove_sync_pending_item(message.id); } } else { if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContactSync(content, message.id, message.timestamp); this.remove_sync_pending_item(message.id); } else { this.saveOutgoingMessageSqlBatch(message); } } } saveOutgoingMessageSql(message, decryptedBody=null, is_encrypted=false) { let pending = 0; let sent = null; let received = null; let encrypted = 0; let content = decryptedBody || message.content; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } const failed_states = ['failed', 'error', 'forbidden']; if (message.state == 'pending') { pending = 1; } else if (message.state == 'accepted') { pending = 0; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (failed_states.indexOf(message.state) > -1) { sent = 1; received = 0; } else { console.log('Invalid state for message', message.id, message.state); return; } let ts = message.timestamp; let unix_timestamp = Math.floor(ts / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(ts), unix_timestamp, content, message.contentType, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL inserted outgoing', message.contentType, 'message to', message.receiver, 'encrypted =', encrypted); this.remove_sync_pending_item(message.id); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } this.remove_sync_pending_item(message.id); }); } async saveOutgoingMessageSqlBatch(message, decryptedBody=null, is_encrypted=false) { let pending = 0; let sent = 0; let received = null; let failed = 0; let encrypted = 0; let content = decryptedBody || message.content; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } if (message.state == 'pending') { pending = 1; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (message.state == 'failed') { sent = 1; received = 0; failed = 1; } else if (message.state == 'error') { sent = 1; received = 0; failed = 1; } else if (message.state == 'forbidden') { sent = 1; received = 0; } let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; this.pendingNewSQLMessages.push(params); if (this.pendingNewSQLMessages.length > 34) { this.insertPendingMessages(); } this.remove_sync_pending_item(message.id); } async saveSystemMessage(uri, content, direction, missed=false) { let timestamp = new Date(); let unix_timestamp = Math.floor(timestamp / 1000); let id = uuid.v4(); let params = [this.state.accountId, id, JSON.stringify(timestamp), unix_timestamp, content, 'text/plain', direction === 'incoming' ? uri : this.state.account.id, direction === 'outgoing' ? uri : this.state.account.id, 0, 1, direction]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, pending, system, direction) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { this.renderSystemMessage(uri, content, direction, timestamp); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async renderPurchasePSTNCredit(uri) { let url = 'https://mdns.sipthor.net/sip_settings.phtml?account='+ this.state.accountId + '&tab=credit'; let myContacts = this.state.myContacts; if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { uri = uri.split('@')[0]; } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > - 1) { let msg; msg = { _id: uuid.v4(), text: 'To call phone numbers, you must purchase credit at ' + url, createdAt: new Date(), direction: 'incoming', sent: true, pending: false, failed: false, user: {_id: uri, name: uri} } renderMessages[uri].push(msg); this.setState({renderMessages: renderMessages}); } } async renderSystemMessage(uri, content, direction, timestamp) { let myContacts = this.state.myContacts; if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { uri = uri.split('@')[0]; } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > - 1) { let msg; msg = { _id: uuid.v4(), text: content, createdAt: timestamp || new Date(), direction: direction, sent: true, pending: false, system: true, failed: false, user: direction == 'incoming' ? {_id: uri, name: uri} : {} } renderMessages[uri].push(msg); this.setState({renderMessages: renderMessages}); } } async saveIncomingMessage(message, decryptedBody=null) { let myContacts = this.state.myContacts; let uri = message.sender.uri; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } var content = decryptedBody || message.content; let received = 1; let unix_timestamp = Math.floor(message.timestamp / 1000); let encrypted = decryptedBody === null ? 0 : 2; let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, this.state.account.id, "incoming", received]; await this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { if (myContacts[uri].name === null || myContacts[uri].name === '' && message.sender.displayName) { myContacts[uri].name = message.sender.displayName; } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } myContacts[uri].unread.push(message.id); myContacts[uri].direction = 'incoming'; myContacts[uri].lastCallDuration = null; if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; this.setState({myContacts: myContacts}); } this.updateTotalUread(myContacts); this.saveSylkContact(uri, myContacts[uri], 'saveIncomingMessage'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } saveIncomingMessageSync(message, decryptedBody=null, is_encrypted=false) { var content = decryptedBody || message.content; let encrypted = 0; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } let received = 0; let imdn_msg; //console.log('saveIncomingMessageSync', message); if (message.dispositionNotification.indexOf('display') === -1) { //console.log('Incoming message', message.id, 'was already read'); received = 2; } else { if (message.dispositionNotification.indexOf('positive-delivery') > -1) { imdn_msg = {id: message.id, timestamp: message.timestamp, from_uri: message.sender.uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { received = 1; } } else { received = 1; } } let pending let sent; let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, this.state.account.id, "incoming", pending, sent, received]; this.pendingNewSQLMessages.push(params); this.remove_sync_pending_item(message.id); if (this.pendingNewSQLMessages.length > 34) { this.insertPendingMessages() } } saveParticipant(callUUID, room, uri) { if (this._historyConferenceParticipants.has(callUUID)) { let old_participants = this._historyConferenceParticipants.get(callUUID); if (old_participants.indexOf(uri) === -1) { old_participants.push(uri); } } else { let new_participants = [uri]; this._historyConferenceParticipants.set(callUUID, new_participants); } if (!this.myParticipants) { this.myParticipants = new Object(); } if (this.myParticipants.hasOwnProperty(room)) { let old_uris = this.myParticipants[room]; if (old_uris.indexOf(uri) === -1 && uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { this.myParticipants[room].push(uri); } } else { let new_uris = []; if (uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { new_uris.push(uri); } if (new_uris) { this.myParticipants[room] = new_uris; } } storage.set('myParticipants', this.myParticipants); } deleteContact(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { this.deleteMessages(uri); } } deletePublicKey(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { myContacts[uri].publicKey = null; console.log('Public key of', uri, 'deleted'); this.saveSylkContact(uri, myContacts[uri], 'deletePublicKey'); } } newContact(uri, name=null, data={}) { //console.log('Create new contact', uri, data); let current_datetime = new Date(); if (data.src !== 'init') { uri = uri.trim().toLowerCase(); } let contact = { id: uuid.v4(), uri: uri, name: name || data.name || '', organization: data.organization || '', unread: [], tags: [], lastCallMedia: [], participants: [], timestamp: current_datetime } contact = this.sanitizeContact(uri, contact, data); return contact; } newSyntheticContact(uri, name=null, data={}) { //console.log('Create new contact', uri, data); let current_datetime = new Date(); uri = uri.trim().toLowerCase(); let contact = { id: uuid.v4(), uri: uri, name: name || data.name || '', organization: data.organization || '', unread: [], tags: ['synthetic'], lastCallMedia: [], participants: [], timestamp: current_datetime } return contact; } updateTotalUread(myContacts=null) { let total_unread = 0; myContacts = myContacts || this.state.myContacts; Object.keys(myContacts).forEach((uri) => { total_unread = total_unread + myContacts[uri].unread.length; }); //console.log('Total unread messages', total_unread) if (Platform.OS === 'ios') { PushNotification.setApplicationIconBadgeNumber(total_unread); } else { ShortcutBadge.setCount(total_unread); } } saveContact(uri, displayName='', organization='', email='') { displayName = displayName.trim(); uri = uri.trim().toLowerCase(); let contact; if (uri.indexOf('@') === -1 && !utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { contact = myContacts[uri]; } else { contact = this.newContact(uri); if (!contact) { return; } } contact.organization = organization; contact.name = displayName; contact.uri = uri; contact.email = email; contact.timestamp = new Date(); contact = this.sanitizeContact(uri, contact); if (!contact) { this._notificationCenter.postSystemNotification('Invalid contact ' + uri); return; } if (!contact.photo) { var name_idx = contact.name.trim().toLowerCase(); if (name_idx in this.state.avatarPhotos) { contact.photo = this.state.avatarPhotos[name_idx]; } } this.replicateContact(contact); this.saveSylkContact(uri, contact, 'saveContact'); let selectedContact = this.state.selectedContact; if (selectedContact && selectedContact.uri === uri) { selectedContact.displayName = displayName; selectedContact.organization = organization; this.setState({selectedContact: selectedContact}); } if (uri === this.state.accountId) { this.setState({displayName: displayName, email: email}); this.signup[this.state.accountId] = email; storage.set('signup', this.signup); if (this.state.account && displayName !== this.state.account.displayName) { this.processRegistration(this.state.accountId, this.state.password, displayName); } } } async replicateContact(contact) { //console.log('Replicate contact', contact); if (!this.state.keys) { console.log('Cannot replicate contact without a private key'); return; } let id = uuid.v4(); let content; let contentType = 'application/sylk-contact-update'; let new_contact = {} new_contact.uri = contact.uri; new_contact.name = contact.name; new_contact.email = contact.email; new_contact.organization = contact.organization; new_contact.timestamp = Math.floor(contact.timestamp / 1000); new_contact.tags = contact.tags; new_contact.participants = contact.participants; content = JSON.stringify(new_contact); //this.saveOutgoingRawMessage(id, this.state.accountId, this.state.accountId, content, contentType); await OpenPGP.encrypt(content, this.state.keys.public).then((encryptedMessage) => { this._sendMessage(this.state.accountId, encryptedMessage, id, contentType, contact.timestamp); }).catch((error) => { console.log('Failed to encrypt contact:', error); }); } handleReplicateContact(json_contact) { let contact; let new_contact; contact = JSON.parse(json_contact); if (contact.uri === null) { return; } if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } let uri = contact.uri; let myContacts = this.state.myContacts; if (uri in myContacts) { new_contact = myContacts[uri]; // } else { new_contact = this.newContact(uri, contact.name); if (!new_contact) { return; } } new_contact.uri = uri; new_contact.name = contact.name; new_contact.email = contact.email; new_contact.organization = contact.organization; new_contact.timestamp = new Date(contact.timestamp * 1000); new_contact.tags = contact.tags; new_contact.participants = contact.participants; this.saveSylkContact(uri, new_contact, 'handleReplicateContact'); } async handleReplicateContactSync(json_contact, id, msg_timestamp) { let purgeMessages = this.state.purgeMessages; let contact; contact = JSON.parse(json_contact); let timestamp = msg_timestamp; let uri = contact.uri; if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } if (contact.timestamp) { timestamp = new Date(contact.timestamp * 1000); } let replicateContacts = this.state.replicateContacts; if (uri in replicateContacts) { if (timestamp < replicateContacts[uri].timestamp) { purgeMessages.push(id); this.setState({purgeMessages: purgeMessages}); //console.log('Sync replicate contact skipped because is too old', timestamp, uri); return; } else { purgeMessages.push(replicateContacts[uri].msg_id); this.setState({purgeMessages: purgeMessages}); //console.log('Sync replicate contact is newer', timestamp, 'than', replicateContacts[uri].timestamp, 'remove previous one', replicateContacts[uri].msg_id); } // } else { let new_contact = this.newContact(uri, contact.name); if (!new_contact) { this.remove_sync_pending_item(id); purgeMessages.push(id); this.setState({purgeMessages: purgeMessages}); return; } replicateContacts[uri] = new_contact; } console.log('Sync replicate contact', uri); replicateContacts[uri].uri = uri; replicateContacts[uri].msg_id = id; replicateContacts[uri].name = contact.name; replicateContacts[uri].email = contact.email; replicateContacts[uri].timestamp = timestamp; replicateContacts[uri].organization = contact.organization; replicateContacts[uri].tags = contact.tags; replicateContacts[uri].participants = contact.participants; //console.log('Adding replicated contact', replicateContacts[uri]); this.setState({replicateContacts: replicateContacts}); this.remove_sync_pending_item(id); } sanitizeContact(uri, contact, data={}) { //console.log('sanitizeContact', uri, contact); let idx; if (!uri || uri === '') { return null; } if (data.src !== 'init') { uri = uri.trim().toLowerCase(); } let domain; let els = uri.split('@'); let username = els[0]; let isNumber = utils.isPhoneNumber(username); if (!isNumber && !utils.isEmailAddress(uri) && username !== '*') { console.log('Sanitize check failed for uri:', uri); return null; } contact.uri = uri; if (!contact.conference) { contact.conference = false; } if (!contact.tags) { contact.tags = []; } contact.tags = [... new Set(contact.tags)]; if (contact.direction === 'received'){ contact.direction = 'incoming'; } else if (contact.direction === 'placed') { contact.direction = 'outgoing'; } if (xtype(contact.timestamp) !== 'date') { contact.timestamp = new Date(); } if (!contact.participants) { contact.participants = []; } contact.participants = [... new Set(contact.participants)]; if (!contact.unread) { contact.unread = []; } contact.unread = [... new Set(contact.unread)]; if (!contact.lastCallMedia) { contact.lastCallMedia = []; } contact.lastCallMedia = [... new Set(contact.lastCallMedia)]; return contact; } updateFavorite(uri, favorite) { if (favorite === null) { return; } let favoriteUris = this.state.favoriteUris; let idx; idx = favoriteUris.indexOf(uri); if (favorite && idx === -1) { favoriteUris.push(uri); this.setState({favoriteUris: favoriteUris, refreshFavorites: !this.state.refreshFavorites}); } else if (!favorite && idx > -1) { favoriteUris.splice(idx, 1); this.setState({favoriteUris: favoriteUris, refreshFavorites: !this.state.refreshFavorites}); } else { return; } } toggleFavorite(uri) { //console.log('toggleFavorite', uri); let favoriteUris = this.state.favoriteUris; let myContacts = this.state.myContacts; let selectedContact; let favorite; if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } idx = myContacts[uri].tags.indexOf('favorite'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); favorite = false; } else { myContacts[uri].tags.push('favorite'); favorite = true; } myContacts[uri].timestamp = new Date(); this.saveSylkContact(uri, myContacts[uri], 'toggleFavorite'); let idx = favoriteUris.indexOf(uri); if (idx === -1 && favorite) { favoriteUris.push(uri); console.log(uri, 'is favorite'); } else if (idx > -1 && !favorite) { favoriteUris.splice(idx, 1); console.log(uri, 'is not favorite'); } this.replicateContact(myContacts[uri]); this.setState({favoriteUris: favoriteUris}); } toggleBlocked(uri) { let blockedUris = this.state.blockedUris; let myContacts = this.state.myContacts; if (uri.indexOf('@guest.') > -1) { uri = 'anonymous@anonymous.invalid'; } if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } let blocked; idx = myContacts[uri].tags.indexOf('blocked'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); blocked = false; } else { myContacts[uri].tags.push('blocked'); blocked = true; } myContacts[uri].timestamp = new Date(); this.saveSylkContact(uri, myContacts[uri], 'toggleBlocked'); let idx = blockedUris.indexOf(uri); if (idx === -1 && blocked) { blockedUris.push(uri); } else if (idx > -1 && !blocked) { blockedUris.splice(idx, 1); } this.replicateContact(myContacts[uri]); this.setState({blockedUris: blockedUris, selectedContact: null}); } updateBlocked(uri, blocked) { if (blocked === null) { return; } let blockedUris = this.state.blockedUris; let idx; idx = blockedUris.indexOf(uri); if (blocked && idx === -1) { blockedUris.push(uri); this.setState({blockedUris: blockedUris}); } else if (!blocked && idx > -1) { blockedUris.splice(idx, 1); this.setState({blockedUris: blockedUris}); } else { return; } } appendInvitedParties(room, uris) { //console.log('Save invited parties', uris, 'for room', room); let myInvitedParties = this.state.myInvitedParties; let current_uris = myInvitedParties.hasOwnProperty(room) ? myInvitedParties[room] : []; uris.forEach((uri) => { let idx = current_uris.indexOf(uri); if (idx === -1) { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } if (uri !== this.state.account.id) { current_uris.push(uri); //console.log('Added', uri, 'to room', room); } } }); this.saveConference(room, uris); } shareContent() { if (this.state.shareContent.length === 0) { return; } if (this.state.selectedContacts.length === 0) { this._notificationCenter.postSystemNotification('Sharing canceled'); } let item = this.state.shareContent[0]; let content = ''; if (item.subject) { content = content + '\n\n' + item.subject; } if (item.text) { content = content + '\n\n' + item.text; } if (item.weblink) { content = content + '\n\n' + item.weblink; } content = content.trim(); this.state.selectedContacts.forEach((uri) => { let msg = this.textToGiftedMessage(content); console.log('Share external item with', uri); this.sendMessage(uri, msg); }); ReceiveSharingIntent.clearReceivedFiles(); this.setState({shareContent: [], selectedContacts: [], shareToContacts: false}); } filterHistory(filter) { this.setState({historyFilter: filter}); } saveConference(room, participants, displayName=null) { let uri = room; console.log('Save conference', room, 'with display name', displayName, 'and participants', participants); let myContacts = this.state.myContacts; if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } myContacts[uri].timestamp = new Date(); myContacts[uri].name = displayName; let new_participants = []; participants.forEach((uri) => { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } if (uri !== this.state.account.id) { new_participants.push(uri); console.log('Added', uri, 'to room', room); } }); myContacts[uri].participants = new_participants; this.replicateContact(myContacts[uri]); this.saveSylkContact(uri, myContacts[uri], 'saveConference'); } addHistoryEntry(uri, callUUID, direction='outgoing', participants=[]) { let myContacts = this.state.myContacts; //console.log('addHistoryEntry', uri); if (uri.indexOf('@') === -1) { uri = uri + '@videoconference.' + this.state.defaultDomain; } if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } myContacts[uri].conference = true; myContacts[uri].timestamp = new Date(); myContacts[uri].lastCallId = callUUID; myContacts[uri].direction = direction; this.saveSylkContact(uri, myContacts[uri], 'addHistoryEntry'); } updateHistoryEntry(uri, callUUID, duration) { if (uri.indexOf('@') === -1) { uri = uri + '@videoconference.' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].lastCallId === callUUID) { console.log('updateHistoryEntry', uri, callUUID, duration); myContacts[uri].timestamp = new Date(); myContacts[uri].lastCallDuration = duration; myContacts[uri].lastCallId = callUUID; this.replicateContact(myContacts[uri]) this.saveSylkContact(uri, myContacts[uri], 'updateHistoryEntry'); } } render() { let footerBox = ; let extraStyles = {}; if (this.state.localMedia || this.state.registrationState === 'registered') { footerBox = null; } let loadingLabel = this.state.loading; if (this.state.syncConversations) { loadingLabel = 'Sync conversations'; } else if (this.state.reconnectingCall) { loadingLabel = 'Reconnecting call...'; } else if (this.mustLogout) { loadingLabel = 'Logging out...'; } return ( this.setState({ Width_Layout : event.nativeEvent.layout.width, Height_Layout : event.nativeEvent.layout.height }, ()=> this._detectOrientation())}> { Platform.OS === 'android' ? : null} ); } notFound(match) { const status = { title : '404', message : 'Oops, the page your looking for can\'t found', level : 'danger', width : 'large' } return ( ); } saveHistory(history) { let myContacts = this.state.myContacts; let missedCalls = this.state.missedCalls; let localTime; let tags = []; let uri; let i = 0; let idx; let contact; let must_save = false; history.forEach((item) => { uri = item.uri; must_save = false; if (this.state.blockedUris.indexOf(uri) > -1) { return; } if (uri in myContacts) { } else { contact = this.newContact(uri); if (!contact) { console.log('No valid contact for', uri); return; } myContacts[uri] = contact; let contacts = this.lookupContacts(uri) if (contacts.length > 0) { myContacts[uri].name = contacts[0].name; myContacts[uri].tags = contacts[0].tags; myContacts[uri].photo = contacts[0].photo; myContacts[uri].label = contacts[0].label; } myContacts[uri].timestamp = item.timestamp; must_save = true; } if (item.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = item.timestamp; must_save = true; } else { if (myContacts[uri].lastCallId === item.sessionId) { return; } else { must_save = true; } } tags = myContacts[uri].tags; if (item.tags.indexOf('missed') > - 1) { tags.push('missed'); myContacts[uri].unread.push(item.sessionId); if (missedCalls.indexOf(item.sessionId) === -1) { missedCalls.push(item.sessionId); must_save = true; } } else { idx = tags.indexOf('missed'); if (idx > -1) { tags.splice(idx, 1); must_save = true; } } tags.push('history'); if (item.displayName && !myContacts[uri].name) { myContacts[uri].name = item.displayName; must_save = true; } myContacts[uri].direction = item.direction; myContacts[uri].lastCallId = item.sessionId; myContacts[uri].lastCallDuration = item.duration; myContacts[uri].lastCallMedia = item.media; myContacts[uri].conference = item.conference; if (tags !== myContacts[uri].tags) { must_save = true; } myContacts[uri].tags = tags; i = i + 1; this.updateTotalUread(myContacts); if (must_save) { this.saveSylkContact(uri, this.state.myContacts[uri], 'saveHistory'); } }); if (i > 0) { console.log('Saved new', i, 'call history items'); } else { console.log('Server call history was already in sync'); } this.setState({missedCalls: missedCalls}); } hideLogsModal() { this.setState({showLogsModal: false}); } purgeLogs() { RNFS.unlink(logfile) .then(() => { utils.timestampedLog('Log file initialized'); this.showLogs(); }) // `unlink` will throw an error, if the item to unlink does not exist .catch((err) => { console.log(err.message); }); } showLogs() { this.setState({showLogsModal: true}); RNFS.readFile(logfile, 'utf8').then((content) => { console.log('Read', content.length, 'bytes from', logfile); const lastlines = content.split('\n').slice(-MAX_LOG_LINES).join('\n'); this.setState({logs: lastlines}); }); } trimLogs() { RNFS.readFile(logfile, 'utf8').then((content) => { const lines = content.split('\n'); //console.log('Read', lines.length, 'lines and', content.length, 'bytes from', logfile); if (lines.length > (MAX_LOG_LINES + 50) || content.length > 100000) { const text = lines.slice(-MAX_LOG_LINES).join('\n'); RNFS.writeFile(logfile, text + '\r\n', 'utf8') .then((success) => { //console.log('Trimmed logs to', MAX_LOG_LINES, 'lines and', text.length, 'bytes'); }) .catch((err) => { console.log(err.message); }); } }); } ready() { let publicKey; let call = this.state.currentCall || this.state.incomingCall; if (this.state.selectedContact) { const uri = this.state.selectedContact.uri; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey) { publicKey = this.state.myContacts[uri].publicKey; } } else { publicKey = this.state.keys ? this.state.keys.public: null; } return ( ); } preview() { return ( ); } saveSlider(position) { this.setState({conferenceSliderPosition: position}); } call() { let call = this.state.currentCall || this.state.incomingCall; let callState; if (call && call.id in this.state.callsState) { callState = this.state.callsState[call.id]; } if (this.state.targetUri in this.state.myContacts && !this.state.callContact) { let callContact = this.state.myContacts[this.state.targetUri]; this.setState({callContact: callContact}); } return ( ) } postSystemNotification(msg) { if (!this._notificationCenter) { return; } this._notificationCenter.postSystemNotification(msg); } conference() { let _previousParticipants = new Set(); let call = this.state.currentCall || this.state.incomingCall; let callState; if (call && call.id in this.state.callsState) { callState = this.state.callsState[call.id]; } /* if (this.myParticipants) { let room = this.state.targetUri.split('@')[0]; if (this.myParticipants.hasOwnProperty(room)) { let uris = this.myParticipants[room]; if (uris) { uris.forEach((uri) => { if (uri.search(this.state.defaultDomain) > -1) { let user = uri.split('@')[0]; _previousParticipants.add(user); } else { _previousParticipants.add(uri); } }); } } } */ if (this.state.myInvitedParties) { if (this.state.myInvitedParties.hasOwnProperty(this.state.targetUri)) { let uris = this.state.myInvitedParties[this.state.targetUri]; if (uris) { uris.forEach((uri) => { _previousParticipants.add(uri); }); } } } let previousParticipants = Array.from(_previousParticipants); return ( ) } matchContact(contact, filter='', matchDisplayName=true) { if (contact.uri.toLowerCase().startsWith(filter.toLowerCase())) { return true; } if (matchDisplayName && contact.name && contact.name.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return true; } return false; } lookupContacts(text) { let contacts = []; const addressbook_contacts = this.state.contacts.filter(contact => this.matchContact(contact, text)); addressbook_contacts.forEach((c) => { const existing_contacts = contacts.filter(contact => this.matchContact(contact, c.uri.toLowerCase(), false)); if (existing_contacts.length === 0) { contacts.push(c); } }); return contacts; } updateLoading(state, by='') { if (by === 'incoming_call_timeout') { console.log('Incoming call timeout'); } else if (this.state.loading === incomingCallLabel && by !== 'incoming_call' && this.state.incomingCallUUID) { console.log('Skip updateLoading because we wait for a call', this.state.loading); return; } else if (by === 'incoming_call' && this.state.loading && this.state.loading !== incomingCallLabel) { console.log('Skip updateLoading by incoming_call', this.state.loading); return; } //console.log('updateLoading', this.state.loading, '->', state, 'by', by); this.setState({loading: state}); } conferenceByUri(urlParameters) { const targetUri = utils.normalizeUri(urlParameters.targetUri, config.defaultConferenceDomain); const idx = targetUri.indexOf('@'); const uri = {}; const pattern = /^[A-Za-z0-9\-\_]+$/g; uri.user = targetUri.substring(0, idx); // check if the uri.user is valid if (!pattern.test(uri.user)) { const status = { title : 'Invalid conference', message : `Oops, the conference ID is invalid: ${targetUri}`, level : 'danger', width : 'large' } return ( ); } return ( ); } login() { let registerBox; let statusBox; this.mustLogout = false; if (this.state.status !== null) { statusBox = ( ); } if (this.state.registrationState !== 'registered') { registerBox = ( ); } return ( {registerBox} {statusBox} ); } logout() { this.syncRequested = false; this.callKeeper.setAvailable(false); this.sql_contacts_keys = []; // SSI wallet - cleanup if (!this.mustLogout && this.state.registrationState !== null && this.state.connection && this.state.connection.state === 'ready') { // remove token from server this.mustLogout = true; //console.log('Remove push token'); this.state.account.setDeviceToken('None', Platform.OS, deviceId, true, bundleId); //console.log('Unregister'); this.state.account.register(); return; } else if (this.mustLogout && this.state.connection && this.state.account) { //console.log('Unregister'); this.state.account.unregister(); } this.tokenSent = false; if (this.state.connection && this.state.account) { //console.log('Remove account'); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { logger.debug(error); } }); } storage.set('account', {accountId: this.state.accountId, password: this.state.password, verified: false }); this.setState({account: null, displayName: '', ssiAgent: null, email: '', loading: null, keyStatus: {}, contactsLoaded: false, registrationState: null, registrationKeepalive: false, keyExistsOnServer: false, keyDifferentOnServer: false, status: null, keys: null, lastSyncId: null, accountVerified: false, autoLogin: false, myContacts: {}, defaultDomain: config.defaultDomain, purgeMessages: [], updateContactUris: {}, replicateContacts: {}, deletedContacts: {} }); this.mustLogout = false; this.ssiAgent = null; this.changeRoute('/login', 'user logout'); return null; } main() { return null; } } export default Sylk; diff --git a/app/components/Call.js b/app/components/Call.js index 418676d..b004d04 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,1361 +1,1361 @@ 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'; 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); } } async handleSSIAgentConnectionStateChange(event) { const connectionRecord = event.payload.connectionRecord; - console.log('SSI session connection', connectionRecord.id, event.payload.previousState, '->', connectionRecord.state); - // console.log('SSI connection event', 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}); - console.log('SSI connection established, we can now verify the remote party'); + 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; - console.log('SSI proof event', proofRecord.id, 'new state:', proofRecord.state); - //console.log('SSI proof event', 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'); console.log('The other party verified us'); } else if (proofRecord.isVerified === true) { // the verification was successful --> call is authorized // this contains firstName, lastName and dob const proofData = proofRecord.presentationMessage.presentationAttachments[0].getDataAsJson(); const proofValues = proofData.requested_proof.revealed_attr_groups.name.values; const firstName = proofValues.firstName.raw; const lastName = proofValues.lastName.raw; const dob = proofValues.dob.raw; let verifiedDisplayName = firstName + ' ' + lastName + ' (' + dob + ')'; - console.log('SSI verify proof succeeded for:', verifiedDisplayName); + 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) { - console.log('SSI verify proof failed'); + 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) { console.log('Call: verify SSI identity over connection', this.state.ssiConnectionRecord.id); this.requestSSIProof(this.state.ssiConnectionRecord.id); } else { this.initSSIConnection(); } } async requestSSIProof(connectionRecordId) { if (this.state.ssiVerifyInProgress) { - console.log('SSI verify in progress') + utils.timestampedLog('SSI verify in progress') } const credDefId = 'Ehx3RZSV38pn3MYvxtHhbQ:3:CL:279883:default'; const attributes = { name: new ProofAttributeInfo({ names: ['firstName', 'lastName', 'dob'], 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 verification timeout'); }, 15000); try { console.log('Request SSI proof over connection', connectionRecordId); const proofRequest = await this.state.ssiAgent.proofs.requestProof(connectionRecordId, proofRequestOptions); console.log(proofRequest); } catch (error) { utils.timestampedLog('SSI request proof 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) { - console.log('SSI connection already active'); + 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; } - console.log('SSI invitation sent'); + 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) { - console.log('SSI received invitation URL', url); + utils.timestampedLog('SSI received invitation URL', url); try { const ssiConnectionRecord = await this.state.ssiAgent.connections.receiveInvitationFromUrl(url); - console.log('SSI invitation accepted', ssiConnectionRecord.id); + 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) { - console.log('SSI local roles:', this.ssiRoles.toString()); - console.log('SSI remote roles:', this.ssiRemoteRoles.toString()); + 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) { - console.log('SSI local roles:', this.ssiRoles.toString()); - console.log('SSI remote roles:', this.ssiRemoteRoles.toString()); + 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) { console.log('Remote party is an SSI holder'); } 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'); } } }); } 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/ios/Podfile.lock b/ios/Podfile.lock index 51e3185..33af394 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,678 +1,690 @@ PODS: - boost-for-react-native (1.63.0) - BVLinearGradient (2.5.6): - React - DoubleConversion (1.1.6) - FBLazyVector (0.61.5) - FBReactNativeSpec (0.61.5): - Folly (= 2018.10.22.00) - RCTRequired (= 0.61.5) - RCTTypeSafety (= 0.61.5) - React-Core (= 0.61.5) - React-jsi (= 0.61.5) - ReactCommon/turbomodule/core (= 0.61.5) - Firebase/Core (6.13.0): - Firebase/CoreOnly - FirebaseAnalytics (= 6.1.6) - Firebase/CoreOnly (6.13.0): - FirebaseCore (= 6.4.0) - Firebase/Messaging (6.13.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.1.9) - FirebaseAnalytics (6.1.6): - FirebaseCore (~> 6.4) - FirebaseInstanceID (~> 4.2) - GoogleAppMeasurement (= 6.1.6) - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (= 0.3.9011) - FirebaseAnalyticsInterop (1.5.0) - FirebaseCore (6.4.0): - FirebaseCoreDiagnostics (~> 1.0) - FirebaseCoreDiagnosticsInterop (~> 1.0) - GoogleUtilities/Environment (~> 6.2) - GoogleUtilities/Logger (~> 6.2) - FirebaseCoreDiagnostics (1.2.4): - FirebaseCoreDiagnosticsInterop (~> 1.2) - GoogleDataTransportCCTSupport (~> 3.0) - GoogleUtilities/Environment (~> 6.5) - GoogleUtilities/Logger (~> 6.5) - nanopb (~> 0.3.901) - FirebaseCoreDiagnosticsInterop (1.2.0) - FirebaseInstanceID (4.2.7): - FirebaseCore (~> 6.0) - GoogleUtilities/Environment (~> 6.0) - GoogleUtilities/UserDefaults (~> 6.0) - FirebaseMessaging (4.1.10): - FirebaseAnalyticsInterop (~> 1.3) - FirebaseCore (~> 6.2) - FirebaseInstanceID (~> 4.1) - GoogleUtilities/AppDelegateSwizzler (~> 6.2) - GoogleUtilities/Environment (~> 6.2) - GoogleUtilities/Reachability (~> 6.2) - GoogleUtilities/UserDefaults (~> 6.2) - Protobuf (>= 3.9.2, ~> 3.9) - Folly (2018.10.22.00): - boost-for-react-native - DoubleConversion - Folly/Default (= 2018.10.22.00) - glog - Folly/Default (2018.10.22.00): - boost-for-react-native - DoubleConversion - glog - glog (0.3.5) - GoogleAppMeasurement (6.1.6): - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (= 0.3.9011) - GoogleDataTransport (6.2.1) - GoogleDataTransportCCTSupport (3.0.0): - GoogleDataTransport (~> 6.0) - nanopb (~> 0.3.901) - GoogleUtilities/AppDelegateSwizzler (6.7.1): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Environment (6.7.1): - PromisesObjC (~> 1.2) - GoogleUtilities/Logger (6.7.1): - GoogleUtilities/Environment - GoogleUtilities/MethodSwizzler (6.7.1): - GoogleUtilities/Logger - GoogleUtilities/Network (6.7.1): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - "GoogleUtilities/NSData+zlib (6.7.1)" - GoogleUtilities/Reachability (6.7.1): - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (6.7.1): - GoogleUtilities/Logger + - indy-sdk-react-native (0.1.21): + - React - nanopb (0.3.9011): - nanopb/decode (= 0.3.9011) - nanopb/encode (= 0.3.9011) - nanopb/decode (0.3.9011) - nanopb/encode (0.3.9011) - Permission-Camera (3.1.0): - RNPermissions - Permission-Contacts (3.1.0): - RNPermissions - Permission-MediaLibrary (3.1.0): - RNPermissions - Permission-Microphone (3.1.0): - RNPermissions - Permission-PhotoLibrary (3.1.0): - RNPermissions - PromisesObjC (1.2.9) - Protobuf (3.12.0) - RCTRequired (0.61.5) - RCTTypeSafety (0.61.5): - FBLazyVector (= 0.61.5) - Folly (= 2018.10.22.00) - RCTRequired (= 0.61.5) - React-Core (= 0.61.5) - React (0.61.5): - React-Core (= 0.61.5) - React-Core/DevSupport (= 0.61.5) - React-Core/RCTWebSocket (= 0.61.5) - React-RCTActionSheet (= 0.61.5) - React-RCTAnimation (= 0.61.5) - React-RCTBlob (= 0.61.5) - React-RCTImage (= 0.61.5) - React-RCTLinking (= 0.61.5) - React-RCTNetwork (= 0.61.5) - React-RCTSettings (= 0.61.5) - React-RCTText (= 0.61.5) - React-RCTVibration (= 0.61.5) - React-Core (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default (= 0.61.5) - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/CoreModulesHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/Default (0.61.5): - Folly (= 2018.10.22.00) - glog - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/DevSupport (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default (= 0.61.5) - React-Core/RCTWebSocket (= 0.61.5) - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - React-jsinspector (= 0.61.5) - Yoga - React-Core/RCTActionSheetHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTAnimationHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTBlobHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTImageHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTLinkingHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTNetworkHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTSettingsHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTTextHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTVibrationHeaders (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-Core/RCTWebSocket (0.61.5): - Folly (= 2018.10.22.00) - glog - React-Core/Default (= 0.61.5) - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsiexecutor (= 0.61.5) - Yoga - React-CoreModules (0.61.5): - FBReactNativeSpec (= 0.61.5) - Folly (= 2018.10.22.00) - RCTTypeSafety (= 0.61.5) - React-Core/CoreModulesHeaders (= 0.61.5) - React-RCTImage (= 0.61.5) - ReactCommon/turbomodule/core (= 0.61.5) - React-cxxreact (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-jsinspector (= 0.61.5) - React-jsi (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-jsi/Default (= 0.61.5) - React-jsi/Default (0.61.5): - boost-for-react-native (= 1.63.0) - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-jsiexecutor (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - React-jsinspector (0.61.5) - react-native-background-downloader (2.3.4): - React - react-native-background-timer (2.1.1): - React - react-native-camera (4.2.1): - React-Core - react-native-camera/RCT (= 4.2.1) - react-native-camera/RN (= 4.2.1) - react-native-camera/RCT (4.2.1): - React-Core - react-native-camera/RN (4.2.1): - React-Core - react-native-cameraroll (4.0.4): - React-Core - react-native-contacts (5.2.3): - React - react-native-document-picker (6.0.4): - React-Core - react-native-fast-openpgp (1.6.5): - React + - react-native-get-random-values (1.7.2): + - React-Core - react-native-randombytes (3.6.1): - React-Core - react-native-safe-area-context (3.3.2): - React-Core - react-native-simple-crypto (0.2.15): - React - react-native-splash-screen (3.2.0): - React - react-native-sqlite-storage (5.0.0): - React - react-native-version-number (0.3.6): - React - react-native-webrtc (1.84.0): - React - React-RCTActionSheet (0.61.5): - React-Core/RCTActionSheetHeaders (= 0.61.5) - React-RCTAnimation (0.61.5): - React-Core/RCTAnimationHeaders (= 0.61.5) - React-RCTBlob (0.61.5): - React-Core/RCTBlobHeaders (= 0.61.5) - React-Core/RCTWebSocket (= 0.61.5) - React-jsi (= 0.61.5) - React-RCTNetwork (= 0.61.5) - React-RCTImage (0.61.5): - React-Core/RCTImageHeaders (= 0.61.5) - React-RCTNetwork (= 0.61.5) - React-RCTLinking (0.61.5): - React-Core/RCTLinkingHeaders (= 0.61.5) - React-RCTNetwork (0.61.5): - React-Core/RCTNetworkHeaders (= 0.61.5) - React-RCTSettings (0.61.5): - React-Core/RCTSettingsHeaders (= 0.61.5) - React-RCTText (0.61.5): - React-Core/RCTTextHeaders (= 0.61.5) - React-RCTVibration (0.61.5): - React-Core/RCTVibrationHeaders (= 0.61.5) - ReactCommon/jscallinvoker (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-cxxreact (= 0.61.5) - ReactCommon/turbomodule/core (0.61.5): - DoubleConversion - Folly (= 2018.10.22.00) - glog - React-Core (= 0.61.5) - React-cxxreact (= 0.61.5) - React-jsi (= 0.61.5) - ReactCommon/jscallinvoker (= 0.61.5) - ReactNativeIncallManager (3.2.3): - React - - RNCallKeep (4.3.1): + - RNCallKeep (4.3.3): - React - RNCAsyncStorage (1.8.0): - React - RNCPushNotificationIOS (1.2.0): - React - RNDeviceInfo (8.4.0): - React-Core - RNDtmf (1.0.0): - React - RNFBApp (6.3.4): - Firebase/Core (~> 6.13.0) - React - RNFBMessaging (6.3.4): - Firebase/Messaging (~> 6.13.0) - React - RNFBApp - RNFileViewer (2.1.4): - React-Core - RNFS (2.18.0): - React - RNPermissions (3.1.0): - React-Core - RNSha1 (1.2.3): - React - RNShare (3.3.3): - React - RNSoundPlayer (0.10.8): - React - RNSVG (12.1.0): - React - RNVectorIcons (6.6.0): - React - RNVoipPushNotification (2.1.0): - React-Core - Yoga (1.14.0) DEPENDENCIES: - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`) - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - indy-sdk-react-native (from `../node_modules/indy-sdk-react-native`) - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`) - Permission-Contacts (from `../node_modules/react-native-permissions/ios/Contacts`) - Permission-MediaLibrary (from `../node_modules/react-native-permissions/ios/MediaLibrary`) - Permission-Microphone (from `../node_modules/react-native-permissions/ios/Microphone`) - Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - React (from `../node_modules/react-native/`) - React-Core (from `../node_modules/react-native/`) - React-Core/DevSupport (from `../node_modules/react-native/`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - react-native-background-downloader (from `../node_modules/react-native-background-downloader`) - react-native-background-timer (from `../node_modules/react-native-background-timer`) - react-native-camera (from `../node_modules/react-native-camera`) - "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)" - react-native-contacts (from `../node_modules/react-native-contacts`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-fast-openpgp (from `../node_modules/react-native-fast-openpgp`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - react-native-randombytes (from `../node_modules/react-native-randombytes`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-simple-crypto (from `../node_modules/react-native-simple-crypto`) - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) - react-native-version-number (from `../node_modules/react-native-version-number`) - react-native-webrtc (from `../node_modules/react-native-webrtc`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - ReactCommon/jscallinvoker (from `../node_modules/react-native/ReactCommon`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - ReactNativeIncallManager (from `../node_modules/react-native-incall-manager`) - RNCallKeep (from `../node_modules/react-native-callkeep`) - "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)" - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - RNDtmf (from `../node_modules/react-native-dtmf/ios`) - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - RNFileViewer (from `../node_modules/react-native-file-viewer`) - RNFS (from `../node_modules/react-native-fs`) - RNPermissions (from `../node_modules/react-native-permissions`) - RNSha1 (from `../node_modules/react-native-sha1`) - RNShare (from `../node_modules/react-native-share`) - RNSoundPlayer (from `../node_modules/react-native-sound-player`) - RNSVG (from `../node_modules/react-native-svg`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - RNVoipPushNotification (from `../node_modules/react-native-voip-push-notification`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: - boost-for-react-native - Firebase - FirebaseAnalytics - FirebaseAnalyticsInterop - FirebaseCore - FirebaseCoreDiagnostics - FirebaseCoreDiagnosticsInterop - FirebaseInstanceID - FirebaseMessaging - GoogleAppMeasurement - GoogleDataTransport - GoogleDataTransportCCTSupport - GoogleUtilities - nanopb - PromisesObjC - Protobuf EXTERNAL SOURCES: BVLinearGradient: :path: "../node_modules/react-native-linear-gradient" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" FBReactNativeSpec: :path: "../node_modules/react-native/Libraries/FBReactNativeSpec" Folly: :podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + indy-sdk-react-native: + :path: "../node_modules/indy-sdk-react-native" Permission-Camera: :path: "../node_modules/react-native-permissions/ios/Camera" Permission-Contacts: :path: "../node_modules/react-native-permissions/ios/Contacts" Permission-MediaLibrary: :path: "../node_modules/react-native-permissions/ios/MediaLibrary" Permission-Microphone: :path: "../node_modules/react-native-permissions/ios/Microphone" Permission-PhotoLibrary: :path: "../node_modules/react-native-permissions/ios/PhotoLibrary" RCTRequired: :path: "../node_modules/react-native/Libraries/RCTRequired" RCTTypeSafety: :path: "../node_modules/react-native/Libraries/TypeSafety" React: :path: "../node_modules/react-native/" React-Core: :path: "../node_modules/react-native/" React-CoreModules: :path: "../node_modules/react-native/React/CoreModules" React-cxxreact: :path: "../node_modules/react-native/ReactCommon/cxxreact" React-jsi: :path: "../node_modules/react-native/ReactCommon/jsi" React-jsiexecutor: :path: "../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../node_modules/react-native/ReactCommon/jsinspector" react-native-background-downloader: :path: "../node_modules/react-native-background-downloader" react-native-background-timer: :path: "../node_modules/react-native-background-timer" react-native-camera: :path: "../node_modules/react-native-camera" react-native-cameraroll: :path: "../node_modules/@react-native-community/cameraroll" react-native-contacts: :path: "../node_modules/react-native-contacts" react-native-document-picker: :path: "../node_modules/react-native-document-picker" react-native-fast-openpgp: :path: "../node_modules/react-native-fast-openpgp" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" react-native-randombytes: :path: "../node_modules/react-native-randombytes" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-simple-crypto: :path: "../node_modules/react-native-simple-crypto" react-native-splash-screen: :path: "../node_modules/react-native-splash-screen" react-native-sqlite-storage: :path: "../node_modules/react-native-sqlite-storage" react-native-version-number: :path: "../node_modules/react-native-version-number" react-native-webrtc: :path: "../node_modules/react-native-webrtc" React-RCTActionSheet: :path: "../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: :path: "../node_modules/react-native/Libraries/NativeAnimation" React-RCTBlob: :path: "../node_modules/react-native/Libraries/Blob" React-RCTImage: :path: "../node_modules/react-native/Libraries/Image" React-RCTLinking: :path: "../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: :path: "../node_modules/react-native/Libraries/Network" React-RCTSettings: :path: "../node_modules/react-native/Libraries/Settings" React-RCTText: :path: "../node_modules/react-native/Libraries/Text" React-RCTVibration: :path: "../node_modules/react-native/Libraries/Vibration" ReactCommon: :path: "../node_modules/react-native/ReactCommon" ReactNativeIncallManager: :path: "../node_modules/react-native-incall-manager" RNCallKeep: :path: "../node_modules/react-native-callkeep" RNCAsyncStorage: :path: "../node_modules/@react-native-community/async-storage" RNCPushNotificationIOS: :path: "../node_modules/@react-native-community/push-notification-ios" RNDeviceInfo: :path: "../node_modules/react-native-device-info" RNDtmf: :path: "../node_modules/react-native-dtmf/ios" RNFBApp: :path: "../node_modules/@react-native-firebase/app" RNFBMessaging: :path: "../node_modules/@react-native-firebase/messaging" RNFileViewer: :path: "../node_modules/react-native-file-viewer" RNFS: :path: "../node_modules/react-native-fs" RNPermissions: :path: "../node_modules/react-native-permissions" RNSha1: :path: "../node_modules/react-native-sha1" RNShare: :path: "../node_modules/react-native-share" RNSoundPlayer: :path: "../node_modules/react-native-sound-player" RNSVG: :path: "../node_modules/react-native-svg" RNVectorIcons: :path: "../node_modules/react-native-vector-icons" RNVoipPushNotification: :path: "../node_modules/react-native-voip-push-notification" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2 FBLazyVector: aaeaf388755e4f29cd74acbc9e3b8da6d807c37f FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75 Firebase: 458d109512200d1aca2e1b9b6cf7d68a869a4a46 FirebaseAnalytics: 45f36d9c429fc91d206283900ab75390cd05ee8a FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae FirebaseCore: 307ea2508df730c5865334e41965bd9ea344b0e5 FirebaseCoreDiagnostics: b59c024493a409f8aecba02c99928d0d8431d159 FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 FirebaseInstanceID: ebd2ea79ee38db0cb5f5167b17a0d387e1cc7b6e FirebaseMessaging: 089b7a4991425783384acc8bcefcd78c0af913bd Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51 glog: 1f3da668190260b06b429bb211bfbee5cd790c28 GoogleAppMeasurement: dfe55efa543e899d906309eaaac6ca26d249862f GoogleDataTransport: 9a8a16f79feffc7f42096743de2a7c4815e84020 GoogleDataTransportCCTSupport: 0f39025e8cf51f168711bd3fb773938d7e62ddb5 GoogleUtilities: e121a3867449ce16b0e35ddf1797ea7a389ffdf2 + indy-sdk-react-native: dae263a92f5a0c6cff71ec3859a10229bd6ff716 nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd Permission-Camera: 0db4fd6e1c556c1cf47f38b989a8084cea3ec3dd Permission-Contacts: c9c3306280576546619fc9ae7489012ed9fe091f Permission-MediaLibrary: eb4dc997f8d3f81f28940f997651cd1a2bf16149 Permission-Microphone: 7b75718196244a6e59dd60f82f12962562c2ea12 Permission-PhotoLibrary: 9dcf80d1353d81b9f1e210c34291591236aaf2b6 PromisesObjC: b48e0338dbbac2207e611750777895f7a5811b75 Protobuf: 2793fcd0622a00b546c60e7cbbcc493e043e9bb9 RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1 RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320 React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78 React-Core: 688b451f7d616cc1134ac95295b593d1b5158a04 React-CoreModules: d04f8494c1a328b69ec11db9d1137d667f916dcb React-cxxreact: d0f7bcafa196ae410e5300736b424455e7fb7ba7 React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7 React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386 React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0 react-native-background-downloader: 4a034cf39a3b00fac57364984e360a4f180a5efb react-native-background-timer: 1b6e6b4e10f1b74c367a1fdc3c72b67c619b222b react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f react-native-cameraroll: 88f4e62d9ecd0e1f253abe4f685474f2ea14bfa2 react-native-contacts: e342f93e42bbaac13729e551ffa35ad4a5b90a45 react-native-document-picker: a7950caaee8587002c9acbc7fe497a342094d93a react-native-fast-openpgp: f79c11db92a059ab2a93142a2d8d4d0f1888ff5a + react-native-get-random-values: 30b3f74ca34e30e2e480de48e4add2706a40ac8f react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057 react-native-simple-crypto: 499ddd9f317932677e50393e58e7a72bca2a86e1 react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 react-native-sqlite-storage: 418ef4afc5e6df6ce3574c4617e5f0b65cffde55 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76 React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360 React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72 React-RCTImage: 6b8e8df449eb7c814c99a92d6b52de6fe39dea4e React-RCTLinking: 121bb231c7503cf9094f4d8461b96a130fabf4a5 React-RCTNetwork: fb353640aafcee84ca8b78957297bd395f065c9a React-RCTSettings: 8db258ea2a5efee381fcf7a6d5044e2f8b68b640 React-RCTText: 9ccc88273e9a3aacff5094d2175a605efa854dbe React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd ReactNativeIncallManager: 155aa4612c0daa6e8f0af761b7b660aa6b13e20f - RNCallKeep: 3ac23a8074aa4b25901a437ee4c6fcbbd7509bfd + RNCallKeep: 0654c8913d1bd24a3441c588b22503828793ad63 RNCAsyncStorage: 5d83b49070d41fd72906a116c5e7bdac4ea3a814 RNCPushNotificationIOS: 4d9ffd08f00ef6c1029ebbf4d72fc711eca3ff01 RNDeviceInfo: 8a06046616c6b5c03787b04f265fa3b52b37c80b RNDtmf: 3d027df415fd00b707061864e2ce270f04d22f3d RNFBApp: 7d4006751de8bb5cafa2e8d2e7d94ee250df81db RNFBMessaging: 51cc7f6beaf4c4feac6f18ea941bba5854e3d9c6 RNFileViewer: 83cc066ad795b1f986791d03b56fe0ee14b6a69f RNFS: 3ab21fa6c56d65566d1fb26c2228e2b6132e5e32 RNPermissions: 4b54095940aea8c03fa3e6c92d4ac3647b31ed4e RNSha1: 6de589cd0b0397d0f6248a2e5ee9e5f0d7da38dc RNShare: f4ec422e27904e0dc9310038d1110460a59ad30d RNSoundPlayer: aa3c998a1f9beaa0f9fdd4485846a74a53df7d07 RNSVG: ce9d996113475209013317e48b05c21ee988d42e RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4 RNVoipPushNotification: 6b38227bbbb2359dc11cbf826044f8ed4bd8de36 Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b PODFILE CHECKSUM: 54352e37dc72ad66bc95fa49966b1ce032a1b2d0 COCOAPODS: 1.10.1 diff --git a/ios/sylk.xcodeproj/project.pbxproj b/ios/sylk.xcodeproj/project.pbxproj index e7a210b..3e8e5c9 100644 --- a/ios/sylk.xcodeproj/project.pbxproj +++ b/ios/sylk.xcodeproj/project.pbxproj @@ -1,1143 +1,1173 @@ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* sylkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* sylkTests.m */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1F207F1527F35DF9000C83CA /* beluga_in.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F207F1327F35DF9000C83CA /* beluga_in.wav */; }; 1F207F1627F35DF9000C83CA /* beluga_out.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F207F1427F35DF9000C83CA /* beluga_out.wav */; }; 1F31871D26495CFF0096C518 /* hangup_tone.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871726495CFF0096C518 /* hangup_tone.wav */; }; 1F31871E26495CFF0096C518 /* outbound_ringtone.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871826495CFF0096C518 /* outbound_ringtone.wav */; }; 1F31871F26495CFF0096C518 /* participant_joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871926495CFF0096C518 /* participant_joined.wav */; }; 1F31872026495CFF0096C518 /* message_received.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871A26495CFF0096C518 /* message_received.wav */; }; 1F31872126495CFF0096C518 /* message_sent.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871B26495CFF0096C518 /* message_sent.wav */; }; 1F31872226495CFF0096C518 /* participant_left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1F31871C26495CFF0096C518 /* participant_left.wav */; }; + 1F5DAB98285FA8DE001F479D /* Indy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F5DAB97285FA8DE001F479D /* Indy.framework */; }; + 1F5DAB99285FA8DE001F479D /* Indy.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1F5DAB97285FA8DE001F479D /* Indy.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 2DCD954D1E0B4F2C00145EB5 /* sylkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* sylkTests.m */; }; 4967DA055DB29E1CA3FABA99 /* libPods-sylk-tvOSTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B7BA6FE0B4159DA52F2FD51 /* libPods-sylk-tvOSTests.a */; }; 642006FA24364F8D00F214A5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 642006F924364F8D00F214A5 /* GoogleService-Info.plist */; }; 64893BB324815ED900F1ACB1 /* incallmanager_ringback.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 64893BB224815ED900F1ACB1 /* incallmanager_ringback.mp3 */; }; 64893BB52481601600F1ACB1 /* incallmanager_busytone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 64893BB42481601600F1ACB1 /* incallmanager_busytone.mp3 */; }; 6FB6376AE557B7A684811B16 /* libPods-sylk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 286EA52C71C0EEB870505AD8 /* libPods-sylk.a */; }; 9F4758B034745B5D2F5C20B6 /* libPods-sylk-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 51AD54962B9A7F91A5A59BD3 /* libPods-sylk-tvOS.a */; }; ADC13CAFF208631E56F30B37 /* libPods-sylkTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E86E14896C117D3A871A0B0A /* libPods-sylkTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; proxyType = 1; remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = sylk; }; 2D02E4911E0B4A5D006451C7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; proxyType = 1; remoteGlobalIDString = 2D02E47A1E0B4A5D006451C7; remoteInfo = "sylk-tvOS"; }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 1F5DAB9A285FA8DE001F479D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 1F5DAB99285FA8DE001F479D /* Indy.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* sylkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = sylkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* sylkTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = sylkTests.m; sourceTree = ""; }; 01ED7FD025E14BC0056B7802 /* Pods-sylk.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk.release.xcconfig"; path = "Target Support Files/Pods-sylk/Pods-sylk.release.xcconfig"; sourceTree = ""; }; 0C6A7EDC60DD46C6B68BC085 /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = ""; }; 11C174A4007FD50566460B39 /* Pods-sylk-tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk-tvOSTests.release.xcconfig"; path = "Target Support Files/Pods-sylk-tvOSTests/Pods-sylk-tvOSTests.release.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* sylk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sylk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = sylk/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = sylk/AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = sylk/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = sylk/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = sylk/main.m; sourceTree = ""; }; 16F8C88D0BB542C7BDD8C492 /* Octicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Octicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = ""; }; 1F207F1327F35DF9000C83CA /* beluga_in.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = beluga_in.wav; path = ../app/assets/sounds/beluga_in.wav; sourceTree = ""; }; 1F207F1427F35DF9000C83CA /* beluga_out.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = beluga_out.wav; path = ../app/assets/sounds/beluga_out.wav; sourceTree = ""; }; 1F31871726495CFF0096C518 /* hangup_tone.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = hangup_tone.wav; path = ../app/assets/sounds/hangup_tone.wav; sourceTree = ""; }; 1F31871826495CFF0096C518 /* outbound_ringtone.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outbound_ringtone.wav; path = ../app/assets/sounds/outbound_ringtone.wav; sourceTree = ""; }; 1F31871926495CFF0096C518 /* participant_joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = participant_joined.wav; path = ../app/assets/sounds/participant_joined.wav; sourceTree = ""; }; 1F31871A26495CFF0096C518 /* message_received.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = message_received.wav; path = ../app/assets/sounds/message_received.wav; sourceTree = ""; }; 1F31871B26495CFF0096C518 /* message_sent.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = message_sent.wav; path = ../app/assets/sounds/message_sent.wav; sourceTree = ""; }; 1F31871C26495CFF0096C518 /* participant_left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = participant_left.wav; path = ../app/assets/sounds/participant_left.wav; sourceTree = ""; }; + 1F5DAB96285FA8CE001F479D /* ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ios; sourceTree = ""; }; + 1F5DAB97285FA8DE001F479D /* Indy.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Indy.framework; path = Pods/Frameworks/Indy.framework; sourceTree = ""; }; 286EA52C71C0EEB870505AD8 /* libPods-sylk.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sylk.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2D02E47B1E0B4A5D006451C7 /* sylk-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sylk-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 2D02E4901E0B4A5D006451C7 /* sylk-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "sylk-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 2E731AC43DE748B89D20AFE1 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; }; 3002AF997675006C10D70A36 /* Pods-sylk.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk.debug.xcconfig"; path = "Target Support Files/Pods-sylk/Pods-sylk.debug.xcconfig"; sourceTree = ""; }; 409CEF65070048FEA6F61E15 /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = ""; }; 4B7BA6FE0B4159DA52F2FD51 /* libPods-sylk-tvOSTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sylk-tvOSTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 51AD54962B9A7F91A5A59BD3 /* libPods-sylk-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sylk-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 642006F924364F8D00F214A5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 64893BB224815ED900F1ACB1 /* incallmanager_ringback.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = incallmanager_ringback.mp3; path = ../app/assets/sounds/incallmanager_ringback.mp3; sourceTree = ""; }; 64893BB42481601600F1ACB1 /* incallmanager_busytone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = incallmanager_busytone.mp3; path = ../app/assets/sounds/incallmanager_busytone.mp3; sourceTree = ""; }; 64E96AF423C4CAEC004F2E23 /* sylk.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = sylk.entitlements; path = sylk/sylk.entitlements; sourceTree = ""; }; 671D4DAF69E7476BA7978555 /* AntDesign.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = AntDesign.ttf; path = "../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf"; sourceTree = ""; }; 71521CAD4ABB4743BEBB2695 /* FontAwesome5_Brands.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Brands.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"; sourceTree = ""; }; 81AA696C92584612895065F5 /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = ""; }; 89A695E1F7D84682917D7A81 /* FontAwesome5_Solid.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Solid.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"; sourceTree = ""; }; 9B22E5C33F9E0CC17621D057 /* Pods-sylkTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylkTests.release.xcconfig"; path = "Target Support Files/Pods-sylkTests/Pods-sylkTests.release.xcconfig"; sourceTree = ""; }; 9B61C54627484A56BC2A9F98 /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; AEFDF496AF8AC1BA5F103045 /* Pods-sylk-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk-tvOS.release.xcconfig"; path = "Target Support Files/Pods-sylk-tvOS/Pods-sylk-tvOS.release.xcconfig"; sourceTree = ""; }; B0040D4144904BA28481D28C /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; BA81785A53AFC04CDA2DD60A /* Pods-sylk-tvOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk-tvOSTests.debug.xcconfig"; path = "Target Support Files/Pods-sylk-tvOSTests/Pods-sylk-tvOSTests.debug.xcconfig"; sourceTree = ""; }; C1EEC850E910413BBAA6C838 /* Zocial.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Zocial.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = ""; }; C928F123C0EEA480ECB32C13 /* Pods-sylkTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylkTests.debug.xcconfig"; path = "Target Support Files/Pods-sylkTests/Pods-sylkTests.debug.xcconfig"; sourceTree = ""; }; CCD62FF165FB4D40AB7112F7 /* Fontisto.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Fontisto.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf"; sourceTree = ""; }; CED7F526111B4006B8F219F0 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; D2AD2CA6EE27A223A8D26B0D /* Pods-sylk-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sylk-tvOS.debug.xcconfig"; path = "Target Support Files/Pods-sylk-tvOS/Pods-sylk-tvOS.debug.xcconfig"; sourceTree = ""; }; E86E14896C117D3A871A0B0A /* libPods-sylkTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sylkTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E9A068D7794A48C1A05373F8 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; }; ECE2C9FB9E504DD58FD917F4 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; F5DBADB972DD4EC78B35AEC7 /* FontAwesome5_Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Regular.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 00E356EB1AD99517003FC87E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ADC13CAFF208631E56F30B37 /* libPods-sylkTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1F5DAB98285FA8DE001F479D /* Indy.framework in Frameworks */, 6FB6376AE557B7A684811B16 /* libPods-sylk.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E4781E0B4A5D006451C7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9F4758B034745B5D2F5C20B6 /* libPods-sylk-tvOS.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E48D1E0B4A5D006451C7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4967DA055DB29E1CA3FABA99 /* libPods-sylk-tvOSTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 00E356EF1AD99517003FC87E /* sylkTests */ = { isa = PBXGroup; children = ( 00E356F21AD99517003FC87E /* sylkTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = sylkTests; sourceTree = ""; }; 00E356F01AD99517003FC87E /* Supporting Files */ = { isa = PBXGroup; children = ( 00E356F11AD99517003FC87E /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 13B07FAE1A68108700A75B9A /* sylk */ = { isa = PBXGroup; children = ( 64E96AF423C4CAEC004F2E23 /* sylk.entitlements */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 642006F924364F8D00F214A5 /* GoogleService-Info.plist */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, ); name = sylk; sourceTree = ""; }; 1F207F1727F35E65000C83CA /* Sounds */ = { isa = PBXGroup; children = ( 1F207F1327F35DF9000C83CA /* beluga_in.wav */, 1F207F1427F35DF9000C83CA /* beluga_out.wav */, 1F31871726495CFF0096C518 /* hangup_tone.wav */, 1F31871A26495CFF0096C518 /* message_received.wav */, 1F31871B26495CFF0096C518 /* message_sent.wav */, 1F31871826495CFF0096C518 /* outbound_ringtone.wav */, 1F31871926495CFF0096C518 /* participant_joined.wav */, 1F31871C26495CFF0096C518 /* participant_left.wav */, 64893BB42481601600F1ACB1 /* incallmanager_busytone.mp3 */, 64893BB224815ED900F1ACB1 /* incallmanager_ringback.mp3 */, ); name = Sounds; sourceTree = ""; }; 1F207F1827F35E80000C83CA /* Fonts */ = { isa = PBXGroup; children = ( 671D4DAF69E7476BA7978555 /* AntDesign.ttf */, 0C6A7EDC60DD46C6B68BC085 /* Entypo.ttf */, 2E731AC43DE748B89D20AFE1 /* EvilIcons.ttf */, 9B61C54627484A56BC2A9F98 /* Feather.ttf */, CED7F526111B4006B8F219F0 /* FontAwesome.ttf */, 71521CAD4ABB4743BEBB2695 /* FontAwesome5_Brands.ttf */, F5DBADB972DD4EC78B35AEC7 /* FontAwesome5_Regular.ttf */, 89A695E1F7D84682917D7A81 /* FontAwesome5_Solid.ttf */, CCD62FF165FB4D40AB7112F7 /* Fontisto.ttf */, E9A068D7794A48C1A05373F8 /* Foundation.ttf */, B0040D4144904BA28481D28C /* Ionicons.ttf */, 81AA696C92584612895065F5 /* MaterialCommunityIcons.ttf */, 409CEF65070048FEA6F61E15 /* MaterialIcons.ttf */, 16F8C88D0BB542C7BDD8C492 /* Octicons.ttf */, ECE2C9FB9E504DD58FD917F4 /* SimpleLineIcons.ttf */, C1EEC850E910413BBAA6C838 /* Zocial.ttf */, ); name = Fonts; sourceTree = ""; }; 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + 1F5DAB97285FA8DE001F479D /* Indy.framework */, + 1F5DAB96285FA8CE001F479D /* ios */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, 286EA52C71C0EEB870505AD8 /* libPods-sylk.a */, 51AD54962B9A7F91A5A59BD3 /* libPods-sylk-tvOS.a */, 4B7BA6FE0B4159DA52F2FD51 /* libPods-sylk-tvOSTests.a */, E86E14896C117D3A871A0B0A /* libPods-sylkTests.a */, ); name = Frameworks; sourceTree = ""; }; 351DDEB757664A18BFAFA3DC /* Resources */ = { isa = PBXGroup; children = ( 1F207F1827F35E80000C83CA /* Fonts */, 1F207F1727F35E65000C83CA /* Sounds */, ); name = Resources; sourceTree = ""; }; 7F8D61712125CE276D8F5B51 /* Pods */ = { isa = PBXGroup; children = ( 3002AF997675006C10D70A36 /* Pods-sylk.debug.xcconfig */, 01ED7FD025E14BC0056B7802 /* Pods-sylk.release.xcconfig */, D2AD2CA6EE27A223A8D26B0D /* Pods-sylk-tvOS.debug.xcconfig */, AEFDF496AF8AC1BA5F103045 /* Pods-sylk-tvOS.release.xcconfig */, BA81785A53AFC04CDA2DD60A /* Pods-sylk-tvOSTests.debug.xcconfig */, 11C174A4007FD50566460B39 /* Pods-sylk-tvOSTests.release.xcconfig */, C928F123C0EEA480ECB32C13 /* Pods-sylkTests.debug.xcconfig */, 9B22E5C33F9E0CC17621D057 /* Pods-sylkTests.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( ); name = Libraries; sourceTree = ""; }; 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( 13B07FAE1A68108700A75B9A /* sylk */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* sylkTests */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 7F8D61712125CE276D8F5B51 /* Pods */, 351DDEB757664A18BFAFA3DC /* Resources */, ); indentWidth = 2; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 83CBBA001A601CBA00E9B192 /* Products */ = { isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* sylk.app */, 00E356EE1AD99517003FC87E /* sylkTests.xctest */, 2D02E47B1E0B4A5D006451C7 /* sylk-tvOS.app */, 2D02E4901E0B4A5D006451C7 /* sylk-tvOSTests.xctest */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 00E356ED1AD99517003FC87E /* sylkTests */ = { isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "sylkTests" */; buildPhases = ( 8CA9E0BAEC26E180E386DD64 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, ); buildRules = ( ); dependencies = ( 00E356F51AD99517003FC87E /* PBXTargetDependency */, ); name = sylkTests; productName = sylkTests; productReference = 00E356EE1AD99517003FC87E /* sylkTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 13B07F861A680F5B00A75B9A /* sylk */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "sylk" */; buildPhases = ( 51438DF049A591005851BA5E /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 64ABC0CA23C8B98D00C218D4 /* Remove ios simulator architectures for webrtc */, 4E5A32BE5DD0BEFB8A279087 /* [CP] Embed Pods Frameworks */, FABB174B0B483AB85B26DC3F /* [CP] Copy Pods Resources */, 2EF9A175F59A228A97C04AE7 /* [CP-User] [RNFB] Core Configuration */, + 1F5DAB9A285FA8DE001F479D /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = sylk; productName = sylk; productReference = 13B07F961A680F5B00A75B9A /* sylk.app */; productType = "com.apple.product-type.application"; }; 2D02E47A1E0B4A5D006451C7 /* sylk-tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = 2D02E4BA1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "sylk-tvOS" */; buildPhases = ( 9252CE9AFF27AA81CF860DC8 /* [CP] Check Pods Manifest.lock */, FD10A7F122414F3F0027D42C /* Start Packager */, 2D02E4771E0B4A5D006451C7 /* Sources */, 2D02E4781E0B4A5D006451C7 /* Frameworks */, 2D02E4791E0B4A5D006451C7 /* Resources */, 2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */, ); buildRules = ( ); dependencies = ( ); name = "sylk-tvOS"; productName = "sylk-tvOS"; productReference = 2D02E47B1E0B4A5D006451C7 /* sylk-tvOS.app */; productType = "com.apple.product-type.application"; }; 2D02E48F1E0B4A5D006451C7 /* sylk-tvOSTests */ = { isa = PBXNativeTarget; buildConfigurationList = 2D02E4BB1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "sylk-tvOSTests" */; buildPhases = ( E992583B817646480BE6CFA2 /* [CP] Check Pods Manifest.lock */, 2D02E48C1E0B4A5D006451C7 /* Sources */, 2D02E48D1E0B4A5D006451C7 /* Frameworks */, 2D02E48E1E0B4A5D006451C7 /* Resources */, ); buildRules = ( ); dependencies = ( 2D02E4921E0B4A5D006451C7 /* PBXTargetDependency */, ); name = "sylk-tvOSTests"; productName = "sylk-tvOSTests"; productReference = 2D02E4901E0B4A5D006451C7 /* sylk-tvOSTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 940; ORGANIZATIONNAME = Facebook; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { DevelopmentTeam = 4DFEFUDLKZ; }; 2D02E47A1E0B4A5D006451C7 = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = 4DFEFUDLKZ; ProvisioningStyle = Automatic; }; 2D02E48F1E0B4A5D006451C7 = { CreatedOnToolsVersion = 8.2.1; ProvisioningStyle = Automatic; TestTargetID = 2D02E47A1E0B4A5D006451C7; }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "sylk" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* sylk */, 00E356ED1AD99517003FC87E /* sylkTests */, 2D02E47A1E0B4A5D006451C7 /* sylk-tvOS */, 2D02E48F1E0B4A5D006451C7 /* sylk-tvOSTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 00E356EC1AD99517003FC87E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 1F31871F26495CFF0096C518 /* participant_joined.wav in Resources */, 64893BB52481601600F1ACB1 /* incallmanager_busytone.mp3 in Resources */, 1F207F1627F35DF9000C83CA /* beluga_out.wav in Resources */, 642006FA24364F8D00F214A5 /* GoogleService-Info.plist in Resources */, 1F31872226495CFF0096C518 /* participant_left.wav in Resources */, 1F31871E26495CFF0096C518 /* outbound_ringtone.wav in Resources */, 1F31872026495CFF0096C518 /* message_received.wav in Resources */, 1F31872126495CFF0096C518 /* message_sent.wav in Resources */, 64893BB324815ED900F1ACB1 /* incallmanager_ringback.mp3 in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 1F31871D26495CFF0096C518 /* hangup_tone.wav in Resources */, 1F207F1527F35DF9000C83CA /* beluga_in.wav in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E4791E0B4A5D006451C7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 2D02E4BD1E0B4A84006451C7 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E48E1E0B4A5D006451C7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Bundle React Native code and images"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; 2D02E4CB1E0B4B27006451C7 /* Bundle React Native Code And Images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Bundle React Native Code And Images"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; }; 2EF9A175F59A228A97C04AE7 /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); name = "[CP-User] [RNFB] Core Configuration"; runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes usful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"firebase_crashlytics_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\n\n # config.admob_delay_app_measurement_init\n _ADMOB_DELAY_APP_MEASUREMENT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_delay_app_measurement_init\")\n if [[ $_ADMOB_DELAY_APP_MEASUREMENT == \"true\" ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADDelayAppMeasurementInit\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"YES\")\n fi\n\n # config.admob_ios_app_id\n _ADMOB_IOS_APP_ID=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_ios_app_id\")\n if [[ $_ADMOB_IOS_APP_ID ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADApplicationIdentifier\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_ADMOB_IOS_APP_ID\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally \n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n\n"; }; 4E5A32BE5DD0BEFB8A279087 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sylk/Pods-sylk-frameworks.sh", "${PODS_ROOT}/../../node_modules/react-native-webrtc/ios/WebRTC.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sylk/Pods-sylk-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 51438DF049A591005851BA5E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-sylk-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 64ABC0CA23C8B98D00C218D4 /* Remove ios simulator architectures for webrtc */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Remove ios simulator architectures for webrtc"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "APP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\n# This script loops through the frameworks embedded in the application and\n# removes unused architectures.\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\n\nEXTRACTED_ARCHS=()\n\nfor ARCH in $ARCHS\ndo\necho \"Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME\"\nlipo -extract \"$ARCH\" \"$FRAMEWORK_EXECUTABLE_PATH\" -o \"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\"\nEXTRACTED_ARCHS+=(\"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\")\ndone\n\necho \"Merging extracted architectures: ${ARCHS}\"\nlipo -o \"$FRAMEWORK_EXECUTABLE_PATH-merged\" -create \"${EXTRACTED_ARCHS[@]}\"\nrm \"${EXTRACTED_ARCHS[@]}\"\n\necho \"Replacing original executable with thinned version\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_EXECUTABLE_PATH-merged\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n\ndone\n\n\n"; }; 8CA9E0BAEC26E180E386DD64 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-sylkTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 9252CE9AFF27AA81CF860DC8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-sylk-tvOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; E992583B817646480BE6CFA2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-sylk-tvOSTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; FABB174B0B483AB85B26DC3F /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sylk/Pods-sylk-resources.sh", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf", "${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sylk/Pods-sylk-resources.sh\"\n"; showEnvVarsInLog = 0; }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Start Packager"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; showEnvVarsInLog = 0; }; FD10A7F122414F3F0027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Start Packager"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 00E356EA1AD99517003FC87E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 00E356F31AD99517003FC87E /* sylkTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E4771E0B4A5D006451C7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2D02E4BF1E0B4AB3006451C7 /* main.m in Sources */, 2D02E4BC1E0B4A80006451C7 /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2D02E48C1E0B4A5D006451C7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2DCD954D1E0B4F2C00145EB5 /* sylkTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 13B07F861A680F5B00A75B9A /* sylk */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; 2D02E4921E0B4A5D006451C7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2D02E47A1E0B4A5D006451C7 /* sylk-tvOS */; targetProxy = 2D02E4911E0B4A5D006451C7 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; children = ( 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; path = sylk; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = C928F123C0EEA480ECB32C13 /* Pods-sylkTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = sylkTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sylk.app/sylk"; }; name = Debug; }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9B22E5C33F9E0CC17621D057 /* Pods-sylkTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; INFOPLIST_FILE = sylkTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = ( "-ObjC", "-lc++", "$(inherited)", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sylk.app/sylk"; }; name = Release; }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3002AF997675006C10D70A36 /* Pods-sylk.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = sylk/sylk.entitlements; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = 4DFEFUDLKZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Pods/Frameworks", + ); INFOPLIST_FILE = sylk/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.8.3; + MARKETING_VERSION = 2.9.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.agprojects.sylk-ios"; PRODUCT_NAME = sylk; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 01ED7FD025E14BC0056B7802 /* Pods-sylk.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = sylk/sylk.entitlements; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 4DFEFUDLKZ; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Pods/Frameworks", + ); INFOPLIST_FILE = sylk/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.8.3; + MARKETING_VERSION = 2.9.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.agprojects.sylk-ios"; PRODUCT_NAME = sylk; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; 2D02E4971E0B4A5E006451C7 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D2AD2CA6EE27A223A8D26B0D /* Pods-sylk-tvOS.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 4DFEFUDLKZ; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "sylk-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.agprojects.sylk-ios-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 9.2; }; name = Debug; }; 2D02E4981E0B4A5E006451C7 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = AEFDF496AF8AC1BA5F103045 /* Pods-sylk-tvOS.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4DFEFUDLKZ; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "sylk-tvOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.agprojects.sylk-ios-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 9.2; }; name = Release; }; 2D02E4991E0B4A5E006451C7 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = BA81785A53AFC04CDA2DD60A /* Pods-sylk-tvOSTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "sylk-tvOSTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.sylk-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sylk-tvOS.app/sylk-tvOS"; TVOS_DEPLOYMENT_TARGET = 10.1; }; name = Debug; }; 2D02E49A1E0B4A5E006451C7 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 11C174A4007FD50566460B39 /* Pods-sylk-tvOSTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = "sylk-tvOSTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.REACT.sylk-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sylk-tvOS.app/sylk-tvOS"; TVOS_DEPLOYMENT_TARGET = 10.1; }; name = Release; }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; 83CBBA211A601CBA00E9B192 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "sylkTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 00E356F61AD99517003FC87E /* Debug */, 00E356F71AD99517003FC87E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "sylk" */ = { isa = XCConfigurationList; buildConfigurations = ( 13B07F941A680F5B00A75B9A /* Debug */, 13B07F951A680F5B00A75B9A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2D02E4BA1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "sylk-tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 2D02E4971E0B4A5E006451C7 /* Debug */, 2D02E4981E0B4A5E006451C7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2D02E4BB1E0B4A5E006451C7 /* Build configuration list for PBXNativeTarget "sylk-tvOSTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 2D02E4991E0B4A5E006451C7 /* Debug */, 2D02E49A1E0B4A5E006451C7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "sylk" */ = { isa = XCConfigurationList; buildConfigurations = ( 83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA211A601CBA00E9B192 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/ios/sylk.xcodeproj/xcshareddata/xcschemes/sylk.xcscheme b/ios/sylk.xcodeproj/xcshareddata/xcschemes/sylk.xcscheme index 3eab476..bfcf4e8 100644 --- a/ios/sylk.xcodeproj/xcshareddata/xcschemes/sylk.xcscheme +++ b/ios/sylk.xcodeproj/xcshareddata/xcschemes/sylk.xcscheme @@ -1,115 +1,115 @@ diff --git a/ios/sylk/Info.plist b/ios/sylk/Info.plist index fa0a667..9822f74 100644 --- a/ios/sylk/Info.plist +++ b/ios/sylk/Info.plist @@ -1,136 +1,136 @@ CFBundleDevelopmentRegion en CFBundleDisplayName Sylk CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.agprojects.sylk-ios CFBundleURLSchemes sylk CFBundleTypeRole Viewer CFBundleURLName com.agprojects.sylk-ios CFBundleURLSchemes sip CFBundleVersion $(CURRENT_PROJECT_VERSION) FirebaseAppDelegateProxyEnabled FirebaseScreenReportingEnabled ITSAppUsesNonExemptEncryption LSApplicationCategoryType LSApplicationQueriesSchemes message sip readdle-spark airmail ms-outlook googlegmail inbox-gmail ymail superhuman LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads - NSCameraUsageDescription - Camera is used for video calls NSAppleMusicUsageDescription Required for file sharing. No data is collected. + NSCameraUsageDescription + Camera is used for video calls NSContactsUsageDescription Contacts permission is needed to call to phone numbers or email addresses found in addressbook. No data is collected. NSLocationWhenInUseUsageDescription NSMicrophoneUsageDescription Microphone is used for audio calls NSPhotoLibraryUsageDescription Library is used to send old photos and videos. No data is collected. UIAppFonts AntDesign.ttf Entypo.ttf EvilIcons.ttf Feather.ttf FontAwesome.ttf FontAwesome5_Brands.ttf FontAwesome5_Regular.ttf FontAwesome5_Solid.ttf Fontisto.ttf Foundation.ttf Ionicons.ttf MaterialIcons.ttf MaterialCommunityIcons.ttf SimpleLineIcons.ttf Octicons.ttf Zocial.ttf UIBackgroundModes fetch remote-notification voip UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance