diff --git a/app/app.js b/app/app.js index 3c6177f..455299e 100644 --- a/app/app.js +++ b/app/app.js @@ -1,10415 +1,10431 @@ // copyright AG Projects 2020-2022 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 {decode as atob, encode as btoa} from 'base-64'; 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 RNFetchBlob from "rn-fetch-blob"; 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 fileType from 'react-native-file-type'; import path from 'react-native-path'; import { Agent, AutoAcceptCredential, AutoAcceptProof, BasicMessageEventTypes, ConnectionEventTypes, ConnectionInvitationMessage, ConnectionRecord, ConnectionStateChangedEvent, ConsoleLogger, CredentialEventTypes, CredentialRecord, CredentialState, CredentialStateChangedEvent, HttpOutboundTransport, WsOutboundTransport, InitConfig, LogLevel, } from '@aries-framework/core'; import { AgentEventTypes } from "@aries-framework/core/build/agent/Events"; 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, fileTransferUrl: config.fileTransferUrl, 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, ssiCredentials: null, ssiConnections: null, deletedContacts: {}, isTexting: false, filteredMessageIds: [], contentTypes: {} }; utils.timestampedLog('Init app'); this.timeoutIncomingTimer = null; this.downloadRequests = {}; this.uploadRequests = {}; 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; this.pendingSsiUrl = null; 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); } storage.initialize(); // Load camera/mic preferences storage.get('devices').then((devices) => { if (devices) { this.setState({devices: devices}); } }); storage.get('account').then((account) => { if (account) { console.log('Account is verified'); this.setState({accountVerified: account.verified}); this.changeRoute('/ready', 'start_up') this.handleRegistration(account.accountId, account.password); } else { this.changeRoute('/login', 'start_up'); } }); 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); 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': 9, '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: []}], 9: [{query: 'alter table messages add column state 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) => { utils.timestampedLog('Saved last message 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) => { utils.timestampedLog('My device UUID was updated', my_uuid); this.setState({myuuid: my_uuid}); setTimeout(() => { this.initSSIAgent(); }, 100); }).catch((error) => { console.log('SQL update uuid error:', error); }); } loadMyKeys() { utils.timestampedLog('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 { utils.timestampedLog('My device UUID', my_uuid); this.setState({myuuid: my_uuid}); setTimeout(() => { this.initSSIAgent(); }, 100); } keys.private = item.private_key; utils.timestampedLog('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 { lastSyncId = item.last_sync_id //lastSyncId = '9e011c85-f83a-4b3c-ac49-0ee7b6bd512c' //lastSyncId = '435471e5-918e-48b5-9a45-1431bd22475f'; console.log('Loaded from SQL las sync id', lastSyncId); this.setState({keys: keys, lastSyncId: lastSyncId}); } 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(); if (lostTasks.length > 0) { console.log('Download lost tasks', lostTasks); } for (let task of lostTasks) { console.log(`Download task ${task.id} was found:`, task.url); if (task.url && task.destination) { task.progress((percent) => { console.log(task.url, `Downloaded: ${percent * 100}%`); }).done(() => { this.saveDownloadTask(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 } utils.timestampedLog('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; utils.timestampedLog("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, \ 'state' 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) { console.log('Route', 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') { 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() { //console.log('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); } }).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') { 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'); } } 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'); } } } } _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() { ReceiveSharingIntent.getReceivedFiles(files => { // files returns as JSON Array example //[{ filePath: null, text: null, weblink: null, mimeType: null, contentUri: null, fileName: null, extension: null }] if (files.length > 0) { console.log('Will share to contacts', files); this.setState({shareToContacts: true, shareContent: files, selectedContact: null}); 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 receiving sharing intent', error.message); }, 'com.agprojects.sylk' // 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.terminateSsiConnections(call.remoteIdentity.uri.toLowerCase()); 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); } } async terminateSsiConnections(uri) { if (!this.ssiAgent) { return; } let allConnections = await this.ssiAgent.connections.getAll(); let callConnections = allConnections.filter(x => x.theirLabel.startsWith(uri)); for (const x of callConnections) { utils.timestampedLog('SSI connection', x.id, 'to', uri, 'removed'); await this.ssiAgent.connections.deleteById(x.id); } allConnections = await this.ssiAgent.connections.getAll(); this.setState({ssiConnections: allConnections}); } 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(); this.initSSIAgent(); 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.ssiRequired) { return; } if (!this.state.accountId) { utils.timestampedLog('Init SSI wallet failed because missing device account id'); return; } if (!this.state.myuuid) { utils.timestampedLog('Init SSI wallet failed because missing device id'); return; } let walletId = this.state.accountId + '_' + this.state.myuuid.replace(/-/g, '_'); //let mediatorUrl = 'wss://ws.didcomm.mediator.bloqzone.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZTUzYWRkMzMtYjZiYS00NWFlLWEwN2MtNTA3NzkxY2YzMjFlIiwgInNlcnZpY2VFbmRwb2ludCI6ICJ3c3M6Ly93cy5kaWRjb21tLm1lZGlhdG9yLmJsb3F6b25lLmNvbSIsICJsYWJlbCI6ICJCbG9xem9uZSBNZWRpYXRvciBBZ2VudCIsICJyZWNpcGllbnRLZXlzIjogWyIzQ2JieUYyVE43RVVTamtTZ3YyNHc2VHZZSGNSZk5yQ3I3eXVTNEJRc1U2RyJdfQ=='; let mediatorUrl = 'https://http.mediator.community.animo.id?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZmFmODZiMjAtZWM4ZC00ZjMzLWI1OGUtN2NmMTAwMzQwZDM5IiwgImxhYmVsIjogIkFuaW1vIENvbW11bml0eSBNZWRpYXRvciIsICJyZWNpcGllbnRLZXlzIjogWyIySzhqUUxaVE15ZkRhcDJnYlczclFMRUhrSml0WjVrQ3R6MVF3NTdWVmlHbSJdLCAic2VydmljZUVuZHBvaW50IjogImh0dHBzOi8vaHR0cC5tZWRpYXRvci5jb21tdW5pdHkuYW5pbW8uaWQifQ=='; utils.timestampedLog('Init SSI wallet id', walletId, 'init via mediator', mediatorUrl); 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: mediatorUrl, 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(); 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); this.ssiAgent.events.on(AgentEventTypes.AgentMessageProcessed, this.incomingSsiMessage); if (ssiRoles.indexOf('verifier') === -1) { ssiRoles.push('verifier'); } const credentials = await this.ssiAgent.credentials.getAll(); let hm = credentials.length > 0 ? credentials.length : "no"; utils.timestampedLog('SSI wallet has', hm, 'credentials'); //console.log(credentials); if (credentials.length > 0) { utils.timestampedLog('SSI added holder role'); if (ssiRoles.indexOf('holder') === -1) { ssiRoles.push('holder'); } } this.setState({ssiRoles: ssiRoles}); const allConnections = await this.ssiAgent.connections.getAll(); utils.timestampedLog('SSI wallet has', allConnections.length, 'connections'); //console.log(allConnections); if (self.pendingSsiUrl) { this.handleSSIEnrolment(self.pendingSsiUrl); self.pendingSsiUrl = null; } this.setState({ssiConnections: allConnections}); allConnections.forEach((item) => { utils.timestampedLog('SSI connection', item.id, 'to', item.theirLabel, 'in state', item.state); }); let noCred = credentials.length > 0 ? credentials.length : "no"; //this._notificationCenter.postSystemNotification("SSI wallet initialised with " + noCred + " credentials"); this.setState({ssiCredentials: credentials}); const rmCommunityConnection = async () => { let connections = allConnections.filter(x => x.theirLabel === 'Animo Community Agent') for (const x of connections) { await this.ssiAgent.connections.deleteById(x.id) } connections = allConnections.filter(x => x.theirLabel === 'Animo Community Mediator') for (const x of connections) { await this.ssiAgent.connections.deleteById(x.id) } } //await rmCommunityConnection(); // run only once if (!allConnections.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) { utils.timestampedLog('SSI wallet init error:', error); this._notificationCenter.postSystemNotification('SSI init' + error); } } async initSSIConnection() { // replaced by the QR code reader return; 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); utils.timestampedLog('SSI connection cached'); // now we can receive a credential from the issuer } catch (error) { utils.timestampedLog('SSI connection error', error); } } async handleSSIAgentCredentialStateChange(event) { 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) { 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) { utils.timestampedLog('SSI wallet credential saved'); this.postSystemNotification('SSI credential saved'); const credentials = await this.ssiAgent.credentials.getAll(); this.setState({ssiCredentials: credentials}); setTimeout(() => { this.filterHistory('ssi'); }, 1000); } } async handleSSIAgentConnectionStateChange(event) { utils.timestampedLog('SSI wallet connection', event.payload.connectionRecord.id, 'state changed to', event.payload.connectionRecord.state); const allConnections = await this.ssiAgent.connections.getAll(); utils.timestampedLog('SSI wallet has', allConnections.length, 'connections'); //console.log(allConnections); this.setState({ssiConnections: allConnections}); if (event.payload.connectionRecord.state === 'complete') { setTimeout(() => { this.filterHistory('ssi'); }, 1000); } } async incomingSsiMessage(event) { if (event.payload.message.type === "https://didcomm.org/basicmessage/1.0/message") { let content = event.payload.message.content; let uri = event.payload.connection.id; let ssiName = uri; let message = new Object(); if (this.state.ssiConnections) { this.state.ssiConnections.forEach((item) => { //console.log('Contacts SSI connection', item); let uri = item.id; if (event.payload.connection.id === item.id) { ssiName = item.theirLabel; return; } }); } console.log('SSI message from', ssiName, ':', content); message.id = event.payload.message.id; message.type = 'normal'; message.contentType = 'text/plain'; message.content = content; message.account = this.state.account; message.ssiName = ssiName; message.timestamp = event.payload.message.sentTime; message.dispositionNotification = []; message.state = 'received'; message.sender = new Object(); message.sender.uri = uri; message.sender.displayName = null; this.incomingMessage(message); } } 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('My PGP key is the same as the one on server'); } } else { console.log('My 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('My 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('My PGP key on server is different than local'); this.setState({showImportPrivateKeyModal: true, keyDifferentOnServer: true}) } else { console.log('My PGP key exists on server and we have a local copy'); 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('My PGP key does not exist on server'); if (this.state.contactsLoaded) { if (this.state.keys && this.state.keys.private) { console.log('My PGP public key sent to server'); this.sendPublicKey(this.state.accountId); } else { 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, {pinned: !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'); this.setState({showQRCodeScanner: !this.state.showQRCodeScanner}); } async handleSSIEnrolment(url) { utils.timestampedLog('SSI enrolment invitation URL', url); if (!this.ssiAgent) { console.log('No SSI agent available yet for handling enrolment to', url); self.pendingSsiUrl = url; return; } try { const ssiConnectionRecord = await this.ssiAgent.connections.receiveInvitationFromUrl(url); 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) { //console.log('Event from url', 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 // This URLs are used to request SSI credentials: // must be updated inside: // * ReadyBox as well // * android/app/src/main/AndroidManifest.xml // * ios/sylk/sylk.entitlements if (url.startsWith('https://didcomm.issuer.bloqzone.com/?c_i=')) { this.handleSSIEnrolment(url); } if (url.startsWith('https://ssimandate.vismaconnect.nl/api/acapy?c_i=')) { this.handleSSIEnrolment(url); } direction = 'outgoing'; event = url_parts[3]; if (!event) { return; } to = url_parts[4]; if (!to) { return; } 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) { 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 messages from server after id', 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, contentType='text/plain') { message.sent = false; message.received = false; message.pending = true; message.direction = 'outgoing'; //console.log('----sendMessage', uri, message); 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; } message.contentType = contentType; message.content = message.text message.content_type = contentType; if (contentType === 'application/sylk-file-transfer') { let file_transfer = message.metadata; const localPath = RNFS.DocumentDirectoryPath + "/" + file_transfer.sender.uri + "/" + file_transfer.receiver.uri + "/" + file_transfer.transfer_id + "/" + file_transfer.filename; const dirname = path.dirname(localPath); await RNFS.mkdir(dirname); await RNFS.copyFile(file_transfer.path, localPath); file_transfer.local_url = localPath; file_transfer.url = this.state.fileTransferUrl + '/' + file_transfer.sender.uri + '/' + file_transfer.receiver.uri + '/' + file_transfer.transfer_id + '/' + file_transfer.filename; message.metadata = file_transfer; this.uploadFile(message.metadata); } if (message.contentType !== 'application/sylk-file-transfer' && 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); //console.log(encryptedMessage); 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, 0, message.contentType); if (message.contentType !== 'application/sylk-file-transfer' ) { this._sendMessage(uri, message.text, message._id, message.contentType, message.createdAt); } } if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = this.buildLastMessage(message) selectedContact.timestamp = message.createdAt; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact}); } console.log('Added render message', message._id, message.contentType); renderMessages[uri].push(message); this.setState({messages: renderMessages}); } async uploadFile(file_transfer){ let encrypted_file; let local_url = file_transfer.local_url; let remote_url = file_transfer.url; let uri = file_transfer.receiver.uri; let outputFile; if (!file_transfer.filetype) { file_transfer.filetype = 'application/octet-stream'; try { let type = await fileType(file_transfer.local_url); file_transfer.filetype = type ? type.mime : 'application/octet-stream'; } catch (e) { console.log('Error getting mime type', e.message); } } if (!this.state.connection) { console.log('Wait for Internet connection...'); return; } if (this.state.connection.state !== 'ready') { console.log('Wait for Internet connection...'); return; } if (remote_url in this.uploadRequests) { console.log('Upload already in progres', file_transfer.url); return; } this.uploadRequests[remote_url] = file_transfer; if (!local_url && file_transfer.transfer_id) { this.deleteMessage(file_transfer.transfer_id, uri); return; } if (utils.isFileEncryptable(file_transfer)) { if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys.public) { encrypted_file = local_url + ".asc"; let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; this.updateRenderFileTransferBubble(file_transfer, 'Encrypting file...'); try { await OpenPGP.encryptFile(local_url, encrypted_file, public_keys, null, {fileName: file_transfer.filename}); } catch (e) { console.log('Error encrypting file', local_url, e) this.outgoingMessageStateChanged(file_transfer.transfer_id, 'failed'); this.updateRenderFileTransferBubble(file_transfer); return; } this.updateRenderFileTransferBubble(file_transfer, 'Calculating checksum...'); try { let base64_content = await RNFS.readFile(encrypted_file, 'base64'); let checksum = utils.getPGPCheckSum(base64_content); const lines = base64_content.match(/.{1,60}/g) ?? []; let content = ""; lines.forEach((line) => { content = content + line + "\n"; }); content = "-----BEGIN PGP MESSAGE-----\n\n"+content+"="+checksum+"\n-----END PGP MESSAGE-----\n"; await RNFS.writeFile(encrypted_file, content, 'utf8'); } catch (e) { console.log('Error generating armored PGP envelope', local_url, e) this.outgoingMessageStateChanged(file_transfer.transfer_id, 'failed'); this.updateRenderFileTransferBubble(file_transfer); return; } this.updateRenderFileTransferBubble(file_transfer, 'File encrypted'); file_transfer.filetype = file_transfer.filetype; local_url = local_url + ".asc"; remote_url = remote_url + '.asc'; this.updateRenderFileTransferBubble(file_transfer); } else { console.log('No public key available for', uri); } } console.log('--- Uploading file', local_url); const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status === 200) { console.log('File uploaded:', local_url); } else { delete this.uploadRequests[remote_url]; const error = new Error(xhr.response); console.log(error); this.outgoingMessageStateChanged(file_transfer.transfer_id, 'failed'); } this.updateRenderFileTransferBubble(file_transfer); delete this.uploadRequests[remote_url] }; let source = encrypted_file || local_url; xhr.open('POST', remote_url); xhr.setRequestHeader('content-type', file_transfer.filetype); this.updateRenderFileTransferBubble(file_transfer, 'Uploading file...'); xhr.send({ uri: 'file://'+ source }); if (xhr.upload) { xhr.upload.onprogress = (event) => { if (event.lengthComputable) { // evt.loaded the bytes the browser received // evt.total the total bytes set by the header var progress = Math.floor((event.loaded/event.total) * 100); //console.log('Upload ' + progress + '%!'); file_transfer.progress = progress; this.updateRenderFileTransferBubble(file_transfer, 'Uploaded ' + progress + '%'); } }; } } 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, content_type="text/plain") { //console.log('saveOutgoingMessage', message._id, content_type, message.metadata); if (content_type !== 'application/sylk-file-transfer') { this.saveOutgoingChatUri(uri, message); } 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, content_type, JSON.stringify(message.metadata), this.state.accountId, uri, "outgoing", "1", encrypted]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, pending, encrypted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('saveOutgoingMessage SQL error:', error); } }); } async saveConferenceMessage(room, message) { let messages = this.state.messages; let ts = message.createdAt; let unix_timestamp = Math.floor(ts / 1000); let contentType = message.metadata && message.metadata.filename ? "application/sylk-file-transfer" : "text/plain"; if (!message.direction) { message.direction = message.received ? 'incoming' : 'outgoing'; } let from_uri = message.direction === 'incoming' ? room : this.state.accountId; let to_uri = message.direction === 'incoming' ? 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, from_uri, to_uri, message.direction); if (room in messages) { messages[room].push(message); } else { messages[room] = [message]; } this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error.message); } }); } async updateConferenceMessage(room, message, update=false) { //console.log('Update conference message', message._id, 'for room', room); let messages = this.state.messages; let sent = message.sent ? 1 : 0; let received = message.received ? 1 : 0; var params = [JSON.stringify(message.metadata), message.text, 0, sent, received, message._id]; await this.ExecuteQuery("update messages set metadata = ?, content = ?, pending = ?, sent = ?, received = ? where msg_id = ?", params).then((result) => { //console.log('SQL update conference message', message._id); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('updateConferenceMessage SQL error:', error.message); } }); let renderMessages = messages[room]; let newRenderMessages = []; if (renderMessages) { renderMessages.forEach((msg) => { if (msg._id === message._id) { msg.image = message.image; msg.video = message.video; msg.text = 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}); } } 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); } }); } 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.updateRenderMessageState(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } } async saveDownloadTask(id, url, local_url) { //console.log('saveDownloadTask', url, local_url); let query = "SELECT * from messages where msg_id = '" + id + "';"; this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; let file_transfer = {}; if (rows.length === 1) { var item = rows.item(0); let metadata = item.metadata || item.content; try { file_transfer = JSON.parse(metadata); } catch (e) { console.log('Error decoding json in saveDownloadTask', metadata); return; } let uri = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; file_transfer.local_url = local_url; file_transfer.paused = false; this.ExecuteQuery("UPDATE messages set metadata = ? where msg_id = ?", [JSON.stringify(file_transfer), id]).then((results) => { console.log('File transfer updated', id); if (local_url.endsWith('.asc')) { try { this.decryptFile(file_transfer); } catch (e) { console.log('Failed to decrypt file', e.message) } } else { this.updateRenderFileTransferBubble(file_transfer); } }).catch((error) => { console.log('saveDownloadTask update SQL error:', error); }); } }).catch((error) => { console.log('saveDownloadTask select 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.updateRenderMessageState(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async fileTransferStateChanged(id, state, file_transfer) { let failed = state === 'failed'; utils.timestampedLog('File transfer', id, 'is', state); let query; const failed_states = ['failed', 'error', 'forbidden']; if (state == 'accepted') { query = "UPDATE messages set metadata = ?, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'delivered') { query = "UPDATE messages set metadata = ?, pending = 0, sent = 1 where msg_id = '" + id + "'"; } else if (state == 'displayed') { query = "UPDATE messages set metadata = ?, received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed_states.indexOf(state) > -1) { file_transfer.failed = true; query = "UPDATE messages set metadata = ?, received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else { console.log('Invalid file transfer state', id, state); return; } //console.log(query); await this.ExecuteQuery(query, [JSON.stringify(file_transfer)]).then((results) => { this.updateRenderFileTransferBubble(file_transfer); }).catch((error) => { console.log('fileTransferStateChanged 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, remote=true) { utils.timestampedLog('Message', id, 'is deleted'); let query; // TODO send request to server //console.log(query); this.removeFilesForMessage(id, uri); if (remote) { this.addJournal(id, 'removeMessage', {uri: uri}); } } - async refetchMessages(days=1) { + async refetchMessagesForContact(contact) { + if (!contact) { + return; + } + let uri = contact.uri; + this.syncRequested = false; + console.log('refetchMessages with', uri); + this.setState({nextSyncUriFilter: uri}); + this.requestSyncConversations(null); + } + + async refetchMessages(days=30) { 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.syncRequested = false; 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); }); } removeFilesForMessage(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.metadata) { let file_transfer = JSON.parse(item.metadata); let remote_party = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; let dir_path = RNFS.DocumentDirectoryPath + "/" + this.state.accountId + "/" + remote_party + "/" + id + "/"; RNFS.unlink(dir_path).then((success) => { console.log('Removed directory', dir_path); }).catch((err) => { console.log('Error deleting directory', dir_path, err.message); }); } 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; this.removeFilesForMessage(id, uri); 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() { //console.log('sendPendingMessages'); if (this.mustLogout) { return; } let content; let metadata; //await this.ExecuteQuery("SELECT * from messages where pending = 1 and content_type like 'text/%' and from_uri = ?", [this.state.accountId]).then((results) => { await this.ExecuteQuery("SELECT * from messages where pending = 1 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.') > -1) { console.log('Skip outgoing conference conference messages'); continue; } if (item.to_uri.indexOf('@videoconference') > -1) { console.log('Skip outgoing videoconference conference messages'); continue; } let timestamp = new Date(item.unix_timestamp * 1000); console.log('Pending outgoing message', item.msg_id, item.content_type, item.to_uri); if (item.content_type === 'application/sylk-file-transfer') { try { metadata = JSON.parse(item.metadata); if (metadata) { this.uploadFile(metadata); } else { this.deleteMessage(item.msg_id, item.msg_id.to_uri); } } catch (e) { console.log("Error decoding outgoing file transfer json sql: ", e); this.deleteMessage(item.msg_id, item.to_uri); } } else { this.sendPendingMessage(item.to_uri, item.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 updateRenderMessageState(id, state) { let query; let uri; let changes = false; //console.log('updateMessage', 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', id, 'new state is', 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') { //this.renderSystemMessage(uri, 'Message delivery failed', 'incoming'); } } } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async saveOutgoingChatUri(uri, message) { //console.log('saveOutgoingChatUri', uri); let query; let content = message.text; 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 = this.buildLastMessage(message); myContacts[uri].lastMessageId = message.id; } if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } 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.updateRenderMessageState(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.updateRenderMessageState(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 checkFileTransfer(file_transfer) { if (file_transfer.local_url) { const exists = await RNFS.exists(file_transfer.local_url); if (exists) { const { size } = await RNFetchBlob.fs.stat(file_transfer.local_url); //console.log('File exists local', file_transfer.transfer_id, file_transfer.local_url); if (size === 0) { let uri = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; this.deleteMessage(file_transfer.transfer_id, uri); } } return; } //console.log('checkFileTransfer', file_transfer); let difference; let now = new Date(); let until = new Date(file_transfer.until); if (now.getTime() > until.getTime()) { console.log('File transfer expired:', file_transfer.transfer_id, file_transfer.filetype); return; } if (file_transfer.paused) { console.log('File transfer is paused', file_transfer.transfer_id, file_transfer.filetype); return; } if (file_transfer.failed) { console.log('File transfer is failed', file_transfer.transfer_id, file_transfer.filetype); return; } let ft_ts = new Date(file_transfer.timestamp); difference = now.getTime() - ft_ts.getTime(); let days = Math.ceil(difference / (1000 * 3600 * 24)); if (days < 10) { if (utils.isImage(file_transfer.filename)) { this.downloadFile(file_transfer); } else { if (file_transfer.filesize < 1000 * 1000) { this.downloadFile(file_transfer); } } } } resumeTransfers() { if (!this.state.selectedContact) { return; } let messages = this.state.messages[this.state.selectedContact.uri] messages.forEach((msg) => { if (msg.metadata && msg.metadata.paused) { console.log('Resume transfer', msg.metadata.transfer_id); this.downloadFile(msg.metadata) } }); } async downloadFile(file_transfer, force=false) { const res = await RNFS.getFSInfo(); console.log('Available space', Math.ceil(res.freeSpace/1024/1024), 'MB'); if (res.freeSpace < file_transfer.filesize) { this._notificationCenter.postSystemNotification('Not enough free space'); return; } let id = file_transfer.transfer_id; let remote_party = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; let dir_path = RNFS.DocumentDirectoryPath + "/" + this.state.accountId + "/" + remote_party + "/" + id + "/"; if (force) { try { await RNFS.unlink(dir_path); utils.timestampedLog('File transfer directory deleted', dir_path); } catch (err) { console.log('Error removing directory', err.message); }; file_transfer.local_url = null; file_transfer.decryption_failed = false; file_transfer.failed = false; if (file_transfer.url.endsWith('.asc') && !file_transfer.filename.endsWith('.asc')) { file_transfer.filename = file_transfer.filename + ('.asc'); } this.updateFileTransferMessageMetadata(file_transfer, 0); } await RNFS.mkdir(dir_path); //console.log('Made directory', dir_path); let file_path = dir_path + "/" + file_transfer.filename; let tmp_file_path = file_path + '.tmp'; if (id in this.downloadRequests) { this.downloadRequests[id].stop(); console.log('File transfer was in progress, stopped it now', id); file_transfer.paused = true; file_transfer.progress = null; this.updateFileTransferMessageMetadata(file_transfer, 0); delete this.downloadRequests[id]; return; } console.log('Downloading file', file_transfer.url); // add a timer to cancel the download //console.log('To local storage:', tmp_file_path); file_transfer.paused = false; file_transfer.progress = 0; //console.log('Adding request id', id, file_transfer.url); this.updateRenderFileTransferBubble(file_transfer, 'Downloading file, press to cancel'); this.downloadRequests[id] = RNBackgroundDownloader.download({ id: id, url: file_transfer.url, destination: tmp_file_path, }).begin((size) => { console.log('File', file_transfer.filename, 'has', size, 'bytes'); this.updateRenderFileTransferBubble(file_transfer, 'Downloading ' + utils.beautySize(file_transfer.filesize), ', press to cancel'); }).progress((percent) => { const progress = Math.ceil(percent * 100); //console.log('File', file_transfer.filename, 'download', progress, '%'); file_transfer.progress = progress; this.updateRenderFileTransferBubble(file_transfer, 'Downloaded ' + progress + '% of '+ utils.beautySize(file_transfer.filesize) +', press to cancel'); }).done(() => { console.log('File', file_transfer.filename, 'downloaded'); delete this.downloadRequests[id]; RNFS.moveFile(tmp_file_path, file_path).then((success) => { this.updateRenderFileTransferBubble(file_transfer, 'Download finished'); this.saveDownloadTask(id, file_transfer.url, file_path); if (this.state.callContact) { this.getMessages(this.state.callContact.uri); } }) .catch((err) => { console.log("Error moving temp file: " + err.message); console.log("Source: ", tmp_file_path); console.log("Destination: ", file_path); file_transfer.local_url = null; this.fileTransferStateChanged(id, 'failed', file_transfer); }); }).error((error) => { console.log('File', file_transfer.filename, 'download failed', error); this.fileTransferStateChanged(id, 'failed', file_transfer); delete this.downloadRequests[id]; }); } async decryptFile(file_transfer) { if (!this.state.keys.private) { return; } let content; let lines = []; let base64_content = ''; let file_path = file_transfer.local_url; let file_path_binary = file_path + '.bin'; let file_path_decrypted = file_path.slice(0, -4); const exists = await RNFS.exists(file_path_decrypted); if (exists) { console.log('File', file_transfer.filename.slice(0, -4), 'is already decrypted'); } this.updateRenderFileTransferBubble(file_transfer, 'Decrypting...'); try { content = await RNFS.readFile(file_path, 'utf8'); } catch (e) { console.log('Error reading file from PGP envelope', e.message); this.updateFileTransferMessageMetadata(file_transfer, 3); return; } try { lines = content.split("\n"); lines.forEach((line) => { if (line === '-----BEGIN PGP MESSAGE-----') { return; } if (line === '') { return; } if (line.indexOf('Version') > -1) { return; } if (line.indexOf('Comment') > -1) { return; } if (line.indexOf('MessageID') > -1) { return; } if (line.indexOf('Hash') > -1) { return; } if (line.indexOf('Charset') > -1) { return; } if (line === '-----END PGP MESSAGE-----') { return; } if (line.startsWith('=')) { return; } base64_content = base64_content + line; }); } catch (e) { console.log('Error breaking PGP envelope', e.message); this.updateFileTransferMessageMetadata(file_transfer, 3); return; } try { await RNFS.writeFile(file_path_binary, base64_content, 'base64'); } catch (e) { console.log('Error extracting file from envelope', e.message); this.updateFileTransferMessageMetadata(file_transfer, 3); return; } await OpenPGP.decryptFile(file_path_binary, file_path_decrypted, this.state.keys.private, null).then((content) => { console.log('File decrypted', file_path_decrypted); file_transfer.local_url = file_path_decrypted; file_transfer.filename = file_transfer.filename.slice(0, -4); try { RNFS.unlink(file_path_binary); } catch (e) { // } try { RNFS.unlink(file_path); } catch (e) { // } this.updateFileTransferMessageMetadata(file_transfer, 2); }).catch((error) => { console.log('Decrypting file', file_path_binary, 'failed:', error.message); this.updateFileTransferMessageMetadata(file_transfer, 3); }); } 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 = this.buildLastMessage(message); 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) { message.image = `data:${message.content_type};base64,${btoa(content)}` } msg = utils.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); }).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, filter={pinned: false, category: null}) { //console.log('Get messages', filter); let pinned=filter && 'pinned' in filter ? filter['pinned'] : false; let category=filter && 'category' in filter ? filter['category'] : null; 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; let localpath; let filteredMessageIds = []; 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'; } if (category && category !== 'text') { query = query + " and metadata != ''"; } 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'; } if (category && category !== 'text') { query = query + " and metadata != ''"; } 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 file_path; let file_transfer; let contentTypes = {}; 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) { 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 === 'application/sylk-file-transfer') { content = content; } else if (item.content_type.indexOf('image/') > -1) { item.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 if (item.content_type === 'text/pgp-public-key-imported') { continue; } else { console.log('Unknown message', item.msg_id, 'type', item.content_type); myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; this.deleteMessage(item.msg_id, item.to_uri); continue; } last_content = content; msg = utils.sql2GiftedChat(item, content, filter); if (!msg) { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } if (msg.audio) { contentTypes['audio'] = true; } else if (msg.image) { contentTypes['image'] = true; } else if (msg.video) { contentTypes['movie'] = true; } else { contentTypes['text'] = true; } if (msg.pinned) { contentTypes['pinned'] = true; } messages[orig_uri].push(msg); if (pinned || category) { filteredMessageIds.push(msg._id); } if (msg.metadata && msg.metadata.filename) { if (msg.metadata.paused) { contentTypes['paused'] = true; } if (msg.metadata.filesize && msg.metadata.filesize > utils.HUGE_FILE_SIZE) { contentTypes['large'] = true; } if (msg.metadata.failed) { contentTypes['failed'] = true; } this.checkFileTransfer(msg.metadata); } } } this.setState({filteredMessageIds: filteredMessageIds, contentTypes: contentTypes}); //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) => { last_message = this.buildLastMessage(last_item); last_message_id = last_item.id; 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('getMessages SQL error:', error); }); } async deleteSsiCredential(contact) { this.setState({ selectedContact: null, targetUri: '' }); if (this.ssiAgent) { await this.ssiAgent.credentials.deleteById(contact.uri); utils.timestampedLog('Deleted SSI credential', contact.uri); const credentials = await this.ssiAgent.credentials.getAll(); this.setState({ssiCredentials: credentials}); } } async deleteSsiConnection(contact) { this.setState({ selectedContact: null, targetUri: '' }); if (this.ssiAgent) { await this.ssiAgent.connections.deleteById(contact.uri); utils.timestampedLog('Deleted SSI connection', contact.uri); const connections = await this.ssiAgent.connections.getAll(); this.setState({ssiConnections: connections}); } } async deleteMessages(uri, remote=false) { console.log('Delete messages for', uri); if (this.state.filteredMessageIds.length > 0) { this.state.filteredMessageIds.forEach((id) => { this.deleteMessage(id, uri, remote); }); return; } let messages = this.state.messages; let myContacts = this.state.myContacts; let query; let params; let orig_uri = uri; if (uri) { if (uri.indexOf('@') === -1 && utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } else { if (remote) { console.log('Delete messages remote party', uri); 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('deleteMessages 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('deleteMessages 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'); } 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) { console.log('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 afterSyncTasks() { this.insertPendingMessages(); if (this.newSyncMessagesCount) { console.log('Synced', this.newSyncMessagesCount, 'messages from server'); this.newSyncMessagesCount = 0; } - this.setState({syncConversations: false}); + this.setState({syncConversations: false, nextSyncUriFilter: null}); 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) { utils.timestampedLog('Sync', messages.length, 'message events from server'); //this._notificationCenter.postSystemNotification('Syncing messages with the server'); this.add_sync_pending_item('sync_in_progress'); } else { this.setState({firstSyncDone: true}); utils.timestampedLog('Sync messages 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 gMsg; 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; } } - //console.log('Process journal', i, 'of', messages.length, message.contentType, uri, message.timestamp); + if (this.state.nextSyncUriFilter && this.state.nextSyncUriFilter !== uri) { + //console.log('Skip journal entry not belonging to', this.state.nextSyncUriFilter); + return; + } + + 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)) { console.log('Skip bad uri', 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; } 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; } if (message.contentType === 'application/sylk-file-transfer') { gMsg = utils.sylk2GiftedChat(message, '', 'incoming'); myContacts[uri].lastMessage = this.buildLastMessage(gMsg); myContacts[uri].lastMessageId = message.id; 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('Last messages update:' , lastMessages); //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; } // This URLs are used to request SSI credentials if (message.content.startsWith('https://didcomm.issuer.bloqzone.com?c_i=')) { this.handleSSIEnrolment(message.content); this.saveSystemMessage(message.sender.uri, 'SSI enrolment proposal received', 'incoming'); //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) { //console.log('handleIncomingMessage') 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; let gMsg = utils.sylk2GiftedChat(message, decryptedBody, 'incoming'); 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(gMsg); let selectedContact = this.state.selectedContact; selectedContact.lastMessage = this.buildLastMessage(gMsg); 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); } buildLastMessage(message, content=null) { let new_content = ''; let filename = 'File'; //console.log('buildLastMessage'); if (message.contentType === 'application/sylk-file-transfer') { new_content = utils.beautyFileNameForBubble(message.metadata, true); } else { new_content = content || message.content || message.text; } let c = new_content.substring(0, 100); return c; } 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); let gMsg; 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; } let gMsg = utils.sylk2GiftedChat(message, content, 'outgoing'); if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = this.buildLastMessage(gMsg); 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(gMsg); //console.log('Added render message', message.id, message.contentType); 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 = 'Photo'; } gMsg = utils.sylk2GiftedChat(message, content, 'outgoing') if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = this.buildLastMessage(gMsg); myContacts[uri].lastMessageId = message.id; } let renderMessages = this.state.messages; //console.log(renderMessages); if (Object.keys(renderMessages).indexOf(uri) > -1) { if (!renderMessages[uri].some((obj) => obj._id === message.id)) { renderMessages[uri].push(gMsg); //console.log('Added render message', message.id, message.contentType); 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.message); 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) { //console.log('saveOutgoingMessageSql'); let pending = 0; let sent = null; let received = null; let encrypted = 0; let content = decryptedBody || message.content; let metadata; if (message.contentType === 'application/sylk-file-transfer') { message.metadata = content; try { metadata = JSON.parse(message.metadata); } catch (e) { console.log('saveOutgoingMessageSql error parsing json', message.metadata); } } else { message.metadata = ''; } 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.metadata, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, metadata, 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); if (message.contentType === 'application/sylk-file-transfer') { if (metadata) { this.updateRenderFileTransferBubble(metadata); this.checkFileTransfer(metadata); } } }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('saveOutgoingMessageSql SQL error:', error); } else { if (message.contentType === 'application/sylk-file-transfer') { this.updateSqlFileTransferMessage(message.id, content, pending, sent, received, message.state); } } this.remove_sync_pending_item(message.id); }); } async updateFileTransferMessageMetadata(metadata, encryption=0) { let query = "SELECT * from messages where msg_id = ?"; await this.ExecuteQuery(query, [metadata.transfer_id]).then((results) => { let rows = results.rows; if (rows.length === 1) { if (encryption === 3) { metadata.decryption_failed = true; } else { metadata.decryption_failed = false; } var item = rows.item(0); let params = [JSON.stringify(metadata), encryption, metadata.transfer_id] query = "update messages set metadata = ?, encrypted = ? where msg_id = ?" this.ExecuteQuery(query, params).then((results) => { //console.log('SQL updated file transfer metadata', metadata); this.updateRenderFileTransferBubble(metadata); }).catch((error) => { console.log('updateFileTransferMessageMetadata SQL error:', error); }); } }).catch((error) => { console.log('updateFileTransferMessageMetadata SQL error:', error); }); } async updateSqlFileTransferMessage(id, content, pending, sent, received, state) { let query = "SELECT * from messages where msg_id = ?"; await this.ExecuteQuery(query, [id]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); var new_metadata = JSON.parse(content); let old_metadata = JSON.parse(item.metadata); new_metadata.local_url = old_metadata.local_url; console.log('File transfer', new_metadata.transfer_id, 'available at', new_metadata.url); let params = [content, JSON.stringify(new_metadata), pending, sent, received, id] query = "update messages set content = ?, metadata = ?, pending = ?, sent = ?, received = ? where msg_id = ?" this.ExecuteQuery(query, params).then((results) => { //console.log('SQL updated file transfer', id); this.checkFileTransfer(new_metadata); // to do, skip query done below this.updateRenderMessageState(id, state); }).catch((error) => { console.log('updateFileTransferMessage SQL error:', error); }); } }).catch((error) => { console.log('updateFileTransferMessage SQL error:', error); }); } 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 (message.contentType === 'application/sylk-file-transfer') { message.metadata = content; } else { message.metadata = ''; } 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.metadata, message.sender.uri, message.receiver, "outgoing", pending, sent, received, message.state]; this.pendingNewSQLMessages.push(params); if (this.pendingNewSQLMessages.length > 24) { this.insertPendingMessages(); } this.remove_sync_pending_item(message.id); } async insertPendingMessages() { let query = "INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, pending, sent, received, state) 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; let pending = 0; let sent = null; let received = null; let state = null; let content = null; let metadata = null; let id = null; let account = null; const failed_states = ['failed', 'error', 'forbidden']; 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; // todo process file transfers pendingNewSQLMessages.forEach((values) => { id = values[2]; if (values[6] === 'application/sylk-file-transfer') { content = values[5]; state = values[14]; if (state == 'pending') { pending = 1; } else if (state == 'accepted') { pending = 0; } else if (state == 'delivered') { sent = 1; } else if (state == 'received') { received = 1; } else if (state == 'displayed') { received = 1; sent = 1; } else if (failed_states.indexOf(state) > -1) { sent = 1; received = 0; } this.updateSqlFileTransferMessage(id, content, pending, sent, received, state); } }); }).catch((error) => { console.log('SQL error inserting bulk messages:', error.message); pendingNewSQLMessages.forEach((values) => { this.ExecuteQuery(query, values).then((result) => { this.newSyncMessagesCount = this.newSyncMessagesCount + 1; }).catch((error) => { id = values[2]; if (error.message.indexOf('SQLITE_CONSTRAINT_PRIMARYKEY') > -1) { // todo update file transfer status if (values[6] === 'application/sylk-file-transfer') { content = values[5]; state = values[14]; if (state == 'pending') { pending = 1; } else if (state == 'accepted') { pending = 0; } else if (state == 'delivered') { sent = 1; } else if (state == 'received') { received = 1; } else if (state == 'displayed') { received = 1; sent = 1; } else if (failed_states.indexOf(state) > -1) { sent = 1; received = 0; } this.updateSqlFileTransferMessage(id, content, pending, sent, received, state); } } else { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error inserting message', id, error.message); } } }); }); }); } } 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}); } } updateRenderFileTransferBubble(metadata, text=null) { if (!this.state.selectedContact) { return; } let id = metadata.transfer_id; let renderMessages = this.state.messages; let existingMessages = renderMessages[this.state.selectedContact.uri]; let newMessages = []; if (!existingMessages) { return; } existingMessages.forEach((msg) => { if (msg._id === id) { msg.text = text || utils.beautyFileNameForBubble(metadata); if (metadata.decryption_failed) { msg.text = msg.text + ' (decryption failed)'; msg.failed = true; } msg.metadata = metadata; if (!metadata.local_url || metadata.decryption_failed || metadata.local_url.endsWith('.asc')) { msg.image = null; msg.video = null; msg.audio = null; } else { if (utils.isImage(metadata.filename)) { if (metadata.b64) { msg.image = `data:${metadata.filetype};base64,${metadata.b64}`; } else { msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } } else if (utils.isAudio(metadata.filename)) { msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } else if (utils.isVideo(metadata.filename, metadata)) { msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } } //console.log('updateRenderFileTransferBubble', msg.text); } newMessages.push(msg); }); renderMessages[this.state.selectedContact.uri] = newMessages; this.setState({messages: 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, message.ssiName); if (message.ssiName) { myContacts[uri].tags.push('ssi'); } } 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 metadata = message.contentType === 'application/sylk-file-transfer' ? message.content : ''; let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, metadata, 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, metadata, 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 = 'Photo'; } else if (message.contentType === 'application/sylk-file-transfer') { try { this.checkFileTransfer(JSON.parse(metadata)); } catch (e) { console.log("Error decoding incoming file transfer json sql: ", e); } } this.updateTotalUread(myContacts); this.saveSylkContact(uri, myContacts[uri], 'saveIncomingMessage'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('saveIncomingMessage 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 metadata = message.contentType === 'application/sylk-file-transfer' ? message.content : ''; //console.log('Sync metadata', message.id, message.contentType, metadata, typeof(message.content)); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, metadata, message.sender.uri, this.state.account.id, "incoming", pending, sent, received, message.state]; this.pendingNewSQLMessages.push(params); this.remove_sync_pending_item(message.id); if (this.pendingNewSQLMessages.length > 24) { 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; try { contact = JSON.parse(json_contact); } catch (e) { console.log("Failed to parse contact json: ", e); return; } 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; try { contact = JSON.parse(json_contact); } catch (e) { console.log("Failed to parse contact json: ", e); return; } 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); let uuidPattern = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; let isUUID = uri.match(uuidPattern); if (!isUUID && !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); } async shareContent() { console.log('Sharing content...'); 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; } var id = uuid.v4(); let msg = { _id: id, key: id, text: content, createdAt: new Date(), direction: 'outgoing', user: {} } content = content.trim(); let contentType = 'text/plain'; if (item.filePath) { contentType = 'application/sylk-file-transfer'; const { size } = await RNFetchBlob.fs.stat(item.filePath); let file_transfer = { 'path': item.filePath, 'filename': item.fileName, 'filetype' : item.mimeType, 'filesize': size, 'sender': {'uri': this.state.accountId}, 'receiver': {'uri': null}, 'transfer_id': id, 'direction': 'outgoing' }; msg.metadata = file_transfer; if (utils.isImage(item.fileName)) { msg.image = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; } else if (utils.isAudio(item.fileName)) { msg.audio = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; } else if (utils.isVideo(item.fileName)) { msg.video = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; } if (content.length > 0) { content = content + ' + ' + utils.beautyFileNameForBubble(file_transfer); } else { content = utils.beautyFileNameForBubble(file_transfer); } } ReceiveSharingIntent.clearReceivedFiles(); this.state.selectedContacts.forEach((uri) => { if (msg.metadata) { msg.metadata.receiver.uri = uri; } this.sendMessage(uri, msg, contentType); }); this.setState({shareContent: [], selectedContacts: [], shareToContacts: false}); } filterHistory(filter) { //console.log('Filter history', 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'); } }); 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/AboutModal.js b/app/components/AboutModal.js index 013a26b..23d5918 100644 --- a/app/components/AboutModal.js +++ b/app/components/AboutModal.js @@ -1,51 +1,51 @@ import React from 'react'; import { Text, Linking, Platform } from 'react-native'; import PropTypes from 'prop-types'; import { Dialog, Portal, Surface, Title } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import styles from '../assets/styles/blink/_AboutModal.scss'; function handleLink(event) { Linking.openURL('https://ag-projects.com'); } function handleUpdate() { if (Platform.OS === 'android') { Linking.openURL('https://play.google.com/store/apps/details?id=com.agprojects.sylk'); } else { Linking.openURL('https://apps.apple.com/us/app/id1489960733'); } } const AboutModal = (props) => { return ( About Sylk Sylk is part of Sylk Suite, a set of real-time communications applications using IETF SIP protocol and WebRTC specifications Version {props.currentVersion} { props.appStoreVersion && props.appStoreVersion.version > props.currentVersion ? handleUpdate()} style={styles.link}>Update Sylk... : handleUpdate()} style={styles.link}>Check App Store for update... } For family, friends and customers, with love. handleLink()} style={styles.link}>Copyright © AG Projects ); } AboutModal.propTypes = { - show: PropTypes.bool.isRequired, + show: PropTypes.bool, close: PropTypes.func.isRequired, currentVersion: PropTypes.string, appStoreVersion: PropTypes.object }; export default AboutModal; diff --git a/app/components/AddContactModal.js b/app/components/AddContactModal.js index 5928983..618c1fe 100644 --- a/app/components/AddContactModal.js +++ b/app/components/AddContactModal.js @@ -1,127 +1,127 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Chip, Dialog, Portal, Text, Button, Surface, TextInput, Paragraph } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import styles from '../assets/styles/blink/_AddContactModal.scss'; class AddContactModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { displayName: this.props.displayName, show: this.props.show, uri: null, displayName: null } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({show: nextProps.show, displayName: nextProps.displayName, uri: nextProps.uri, organization: nextProps.organization }); } save(event) { event.preventDefault(); this.props.saveContact(this.state.uri, this.state.displayName, this.state.organization); this.props.close(); } onUriChange(value) { value = value.replace(/\s|\(|\)/g, '').toLowerCase(); this.setState({uri: value}); } onDisplayChange(value) { this.setState({displayName: value}); } onOrganizationChange(value) { this.setState({organization: value}); } render() { return ( Add contact The domain is optional, it defaults to @{this.props.defaultDomain} {!this.state.uri ? : } ); } } AddContactModal.propTypes = { - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, saveContact: PropTypes.func, defaultDomain: PropTypes.string }; export default AddContactModal; diff --git a/app/components/CallMeMaybeModal.js b/app/components/CallMeMaybeModal.js index fbf9cdd..64b845b 100644 --- a/app/components/CallMeMaybeModal.js +++ b/app/components/CallMeMaybeModal.js @@ -1,125 +1,125 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import { Dialog, Title, Surface, Portal, IconButton, Text } from 'react-native-paper'; import autoBind from 'auto-bind'; import { openComposer } from 'react-native-email-link'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import Share from 'react-native-share'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import utils from '../utils'; import styles from '../assets/styles/blink/_CallMeMaybeModal.scss'; class CallMeMaybeModal extends Component { constructor(props) { super(props); autoBind(this); } handleClipboardButton(event) { utils.copyToClipboard(this.props.callUrl); this.props.notificationCenter().postSystemNotification('Call me', {body: 'Web address copied to the clipboard'}); this.props.close(); } handleEmailButton(event) { const sipUri = this.props.callUrl.split('/').slice(-1)[0]; // hack! const emailMessage = `You can call me using a Web browser at ${this.props.callUrl} or a SIP client at ${sipUri} ` + 'or by using the freely available Sylk client app from http://sylkserver.com'; const subject = 'Call me, maybe?'; openComposer({ subject, body: emailMessage }) // Linking.canOpenURL(this.emailLink) // .then((supported) => { // if (!supported) { // } else { // return Linking.openURL(url); // } // }) // .catch((err) => { // this.props.notificationCenter().postSystemNotification('Call me', {body: 'Unable to open email app'}); // }); this.props.close(); } handleShareButton(event) { const sipUri = this.props.callUrl.split('/').slice(-1)[0]; // hack! let options= { subject: 'Call me, maybe?', message: `You can call me using a Web browser at ${this.props.callUrl} or a SIP client at ${sipUri} or by using the freely available Sylk WebRTC client app at http://sylkserver.com` } Share.open(options) .then((res) => { this.props.close(); }) .catch((err) => { this.props.close(); }); } render() { const sipUri = this.props.callUrl.split('/').slice(-1)[0]; return ( Call me, maybe? Others can call you with SIP at: {sipUri} or with a Web browser at: {this.props.callUrl} Share this address with others: ); } } CallMeMaybeModal.propTypes = { - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, callUrl : PropTypes.string.isRequired, notificationCenter : PropTypes.func.isRequired }; export default CallMeMaybeModal; diff --git a/app/components/EditConferenceModal.js b/app/components/EditConferenceModal.js index ba1dcb7..ad477d9 100644 --- a/app/components/EditConferenceModal.js +++ b/app/components/EditConferenceModal.js @@ -1,158 +1,158 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Chip, Dialog, Portal, Text, Button, Surface, TextInput, Paragraph } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import config from '../config'; import styles from '../assets/styles/blink/_InviteParticipantsModal.scss'; class EditConferenceModal extends Component { constructor(props) { super(props); autoBind(this); let participants = []; if (this.props.invitedParties && this.props.invitedParties.length > 0) { participants = this.props.invitedParties; } else if (this.props.selectedContact && this.props.selectedContact.participants) { participants = this.props.selectedContact.participants; } this.state = { participants: this.sanitizedParticipants(participants), selectedContact: this.props.selectedContact, show: this.props.show, displayName: this.props.displayName, invitedParties: this.props.invitedParties } } sanitizedParticipants(participants) { let sanitizedParticipants = []; participants.forEach((item) => { item = item.trim().toLowerCase(); if (item === this.props.accountId) { return; } if (item.indexOf('@') === -1) { sanitizedParticipants.push(item); } else { const domain = item.split('@')[1]; if (domain === this.props.defaultDomain) { sanitizedParticipants.push(item.split('@')[0]); } else { sanitizedParticipants.push(item); } } }); return sanitizedParticipants.toString().replace(/,/g, ", "); } UNSAFE_componentWillReceiveProps(nextProps) { let participants = []; if (nextProps.invitedParties && nextProps.invitedParties.length > 0) { participants = nextProps.invitedParties; } else if (nextProps.selectedContact && nextProps.selectedContact.participants) { participants = nextProps.selectedContact.participants; } this.setState({ participants: this.sanitizedParticipants(participants), selectedContact: nextProps.selectedContact, show: nextProps.show, displayName: nextProps.displayName, invitedParties: nextProps.invitedParties }); } saveConference(event) { event.preventDefault(); const uris = []; if (this.state.participants) { this.state.participants.split(',').forEach((item) => { item = item.trim(); if (uris.indexOf(item) === -1) { uris.push(item); } }); } if (uris || this.state.displayName) { let name = this.state.displayName || this.state.selectedContact.uri; this.props.saveConference(this.state.selectedContact.uri, uris, name); } this.props.close(); } displayNameChange(value) { this.setState({displayName: value}); } participantsChange(value) { this.setState({participants: value}); } render() { return ( Conference {this.props.room} ); } } EditConferenceModal.propTypes = { room : PropTypes.string, displayName : PropTypes.string, - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, saveConference : PropTypes.func, invitedParties : PropTypes.array, selectedContact : PropTypes.object, defaultDomain : PropTypes.string, accountId : PropTypes.string }; export default EditConferenceModal; diff --git a/app/components/EditContactModal.js b/app/components/EditContactModal.js index 753ec3b..147c3eb 100644 --- a/app/components/EditContactModal.js +++ b/app/components/EditContactModal.js @@ -1,258 +1,258 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { Linking } from 'react-native'; import { View } from 'react-native'; import { Chip, Dialog, Portal, Text, Button, Surface, TextInput, Paragraph, Subheading } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import styles from '../assets/styles/blink/_EditContactModal.scss'; import utils from '../utils'; function handleUpdate() { Linking.openURL('http://delete.sylk.link'); } class EditContactModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { displayName: this.props.displayName, organization: this.props.organization, show: this.props.show, email: this.props.email, myself: this.props.myself, uri: this.props.uri, confirm: false } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({show: nextProps.show, displayName: nextProps.displayName, email: nextProps.email, uri: nextProps.uri, myself: nextProps.myself, organization: nextProps.organization }); } saveContact(event) { event.preventDefault(); this.props.saveContact(this.state.displayName, this.state.organization, this.state.email); this.setState({confirm: false}); this.props.close(); } deleteContact(event) { event.preventDefault(); if (!this.state.confirm) { this.setState({confirm: true}); return; } this.setState({confirm: false}); this.props.deleteContact(this.state.uri); this.props.close(); } validEmail() { if (!this.state.email) { return true; } let check = utils.isEmailAddress(this.state.email); return check } deletePublicKey(event) { event.preventDefault(); if (!this.state.confirm) { this.setState({confirm: true}); return; } this.setState({confirm: false}); this.props.deletePublicKey(this.state.uri); this.props.close(); } handleClipboardButton(event) { event.preventDefault(); console.log('Key copied to clipboard') utils.copyToClipboard(this.props.publicKey); this.props.close(); } onInputChange(value) { this.setState({displayName: value}); } onOrganizationChange(value) { this.setState({organization: value}); } onEmailChange(value) { this.setState({email: value}); } render() { if (this.props.publicKey) { let title = this.props.displayName || this.props.uri return ( {title} PGP Public Key {this.props.publicKey} ); } return ( {this.props.uri} { !this.state.myself ? : } { this.state.myself ? Used to recover a lost password : null} { !this.state.myself ? : null} handleUpdate()} style={styles.link}>Delete acount on server... {true ? Messages are encrypted end-to-end : null} {true ? My device id: {this.props.myuuid} : null} ); } } EditContactModal.propTypes = { - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, uri : PropTypes.string, displayName : PropTypes.string, email : PropTypes.string, organization : PropTypes.string, publicKey : PropTypes.string, myself : PropTypes.bool, saveContact : PropTypes.func, deleteContact : PropTypes.func, deletePublicKey : PropTypes.func, myuuid : PropTypes.string }; export default EditContactModal; diff --git a/app/components/EscalateConferenceModal.js b/app/components/EscalateConferenceModal.js index 431ae7e..94a4854 100644 --- a/app/components/EscalateConferenceModal.js +++ b/app/components/EscalateConferenceModal.js @@ -1,85 +1,85 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; import autoBind from 'auto-bind'; import { Portal, Dialog, Paragraph, TextInput, Surface, Button } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import styles from '../assets/styles/blink/_EscalateConferenceModal.scss'; class EscalateConferenceModal extends React.Component { constructor(props) { super(props); autoBind(this); this.state = { call: this.props.call, users: props.selectedContacts ? props.selectedContacts.toString() : '' } } escalateToConference(event) { event.preventDefault(); const uris = []; if (this.state.users) { for (let item of this.state.users.split(',')) { item = item.trim(); uris.push(item); }; } if (uris.indexOf(this.props.call.remoteIdentity.uri) === -1) { uris.push(this.props.call.remoteIdentity.uri); } this.props.escalateToConference(uris); } onInputChange(value) { this.setState({users: value}); } render() { return ( Move call to conference Enter the accounts you wish to invite separated by commas ); } } EscalateConferenceModal.propTypes = { - show: PropTypes.bool.isRequired, + show: PropTypes.bool, close: PropTypes.func.isRequired, call: PropTypes.object, selectedContacts: PropTypes.array, escalateToConference: PropTypes.func }; export default EscalateConferenceModal; diff --git a/app/components/ExportPrivateKeyModal.js b/app/components/ExportPrivateKeyModal.js index 7fe8bcc..a88116e 100644 --- a/app/components/ExportPrivateKeyModal.js +++ b/app/components/ExportPrivateKeyModal.js @@ -1,110 +1,110 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Dialog, Portal, Text, Button, Surface} from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import styles from '../assets/styles/blink/_ExportPrivateKeyModal.scss'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; class ExportPrivateKeyModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { password: this.props.password, show: this.props.show, sent: this.props.sent, status: '' } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({show: nextProps.show, password: nextProps.password, status: nextProps.status || '', publicKeyHash: nextProps.publicKeyHash, sent: nextProps.password === this.state.password }); } save(event) { event.preventDefault(); this.props.saveFunc(this.state.password); this.setState({sent: true, status: 'Enter same pincode on the other devices'}); } get disableButton() { if (!this.state.password || this.state.password.length < 6) { return true; } if (this.state.sent) { return true; } return false; } onInputChange(value) { this.setState({password: value}); } render() { return ( Export private key To replicate messages on multiple devices you need the same private key on all of them. Enter this code when prompted on your other device: {this.state.password} {this.state.status} ); } } ExportPrivateKeyModal.propTypes = { - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, password : PropTypes.string, saveFunc : PropTypes.func.isRequired, publicKeyHash : PropTypes.string }; export default ExportPrivateKeyModal; diff --git a/app/components/ImportPrivateKeyModal.js b/app/components/ImportPrivateKeyModal.js index ce6ca98..a01cb92 100644 --- a/app/components/ImportPrivateKeyModal.js +++ b/app/components/ImportPrivateKeyModal.js @@ -1,211 +1,211 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Dialog, Portal, Text, Button, Surface, TextInput } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import styles from '../assets/styles/blink/_PrivateKeyModal.scss'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; class ImportPrivateKeyModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { password: this.props.password, show: this.props.show, privateKey: this.props.privateKey, status: this.props.status, confirm: false, keyStatus: this.props.keyStatus, success: this.props.success, keyDifferentOnServer: this.props.keyDifferentOnServer } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({show: nextProps.show, password: nextProps.password || this.state.password, privateKey: nextProps.privateKey, status: nextProps.status, confirm: nextProps.confirm, success: nextProps.success, keyStatus: nextProps.keyStatus, keyDifferentOnServer: nextProps.keyDifferentOnServer }); if (nextProps.success) { setTimeout(() => { this.props.close(); }, 3000); } } save(event) { event.preventDefault(); this.props.saveFunc(this.state.password); } generateKeys(event) { event.preventDefault(); if (this.state.confirm) { this.props.generateKeysFunc(); this.props.close(); } else { this.setState({confirm: true}); } } useExistingKeys(event) { event.preventDefault(); this.props.useExistingKeysFunc(); this.props.close(); } get disableButton() { if (!this.state.password || this.state.password.length < 6) { return true; } if (this.state.success) { return true; } return false; } onInputChange(value) { this.setState({password: value}); } render() { const statusStyle = !this.state.status ? styles.statusFail: styles.status; if (this.state.privateKey) { return ( Import private key {'Enter the pincode shown on the sending device to import your private key:'} { if (this.state.password.length === 6) { this.props.saveFunc(this.state.password); } }} onChangeText={this.onInputChange} required defaultValue={this.state.password} autoCapitalize="none" /> {!this.state.status ? : {this.state.status} } ); } else { if (this.state.keyDifferentOnServer) { return ( Another device? You have used messaging on more than one device. To decrypt messages, you need the same private key on all devices. To use the private key from another device, choose on that device to menu option 'Export private key'. ); } else { return ( Another device? To decrypt messages, you need the same private key on all devices. To use the private key from another device, choose on that device to menu option 'Export private key'. In case you lost access to your old devices, you must generate a new key. If you do this, older message cannot be read anymore. ); } } } } ImportPrivateKeyModal.propTypes = { - show : PropTypes.bool.isRequired, + show : PropTypes.bool, close : PropTypes.func.isRequired, privateKey : PropTypes.string, saveFunc : PropTypes.func.isRequired, generateKeysFunc : PropTypes.func.isRequired, useExistingKeysFunc : PropTypes.func.isRequired, status : PropTypes.string, keyDifferentOnServer: PropTypes.bool, keyExistsOnServer : PropTypes.bool, keyStatus : PropTypes.object, success : PropTypes.bool }; export default ImportPrivateKeyModal; diff --git a/app/components/InviteParticipantsModal.js b/app/components/InviteParticipantsModal.js index cb43ca5..5d37334 100644 --- a/app/components/InviteParticipantsModal.js +++ b/app/components/InviteParticipantsModal.js @@ -1,297 +1,297 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View, TouchableOpacity } from 'react-native'; import { Dialog, Portal, Text, Button, Surface, TextInput, IconButton} from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import { openComposer } from 'react-native-email-link'; import Share from 'react-native-share'; import Autocomplete from 'react-native-autocomplete-input'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import config from '../config'; import utils from '../utils'; import styles from '../assets/styles/blink/_ConferenceModal.scss'; class InviteParticipantsModal extends Component { constructor(props) { super(props); autoBind(this); const sanitizedParticipants = []; let participants = []; participants = this.props.previousParticipants.filter(x => !this.props.currentParticipants.includes(x)); participants = participants.filter(x => !this.props.alreadyInvitedParticipants.includes(x) && x !== this.props.accountId); participants.forEach((item) => { item = item.trim().toLowerCase(); if (item.indexOf('@') === -1) { sanitizedParticipants.push(item); } else { const domain = item.split('@')[1]; if (domain === this.props.defaultDomain) { sanitizedParticipants.push(item.split('@')[0]); } else { sanitizedParticipants.push(item); } } }); this.state = { participants: sanitizedParticipants.toString().replace(/,/g, ", "), previousParticipants: this.props.previousParticipants, currentParticipants: this.props.currentParticipants, roomUrl: config.publicUrl + '/conference/' + this.props.room, filteredContacts: [], searching: false } } UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } let difference = nextProps.previousParticipants.filter(x => !nextProps.currentParticipants.includes(x)); difference = difference.filter(x => !nextProps.alreadyInvitedParticipants.includes(x) && x !== this.props.accountId); this.setState({ alreadyInvitedParticipants: nextProps.alreadyInvitedParticipants, previousParticipants: nextProps.previousParticipants, currentParticipants: nextProps.currentParticipants, roomUrl: config.publicUrl + '/conference/' + nextProps.room }); } handleClipboardButton(event) { utils.copyToClipboard(this.state.roomUrl); this.props.notificationCenter().postSystemNotification('Conference', {body: 'address copied to clipboard'}); this.props.close(); } handleEmailButton(event) { const emailMessage = 'You can join my conference at ' + this.state.roomUrl; const subject = 'Join conference, maybe?'; openComposer({ subject, body: emailMessage }) this.props.close(); } handleShareButton(event) { const subject = 'Join conference, maybe?'; const message = 'You can join my conference at ' + this.state.roomUrl; let options= { subject: subject, message: message } Share.open(options) .then((res) => { this.props.close(); }) .catch((err) => { this.props.close(); }); } invite(event) { event.preventDefault(); const uris = []; if (this.state.participants) { this.state.participants.split(',').forEach((item) => { item = item.trim().toLowerCase(); if (item.length > 1) { if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } else { const domain = item.split('@')[1]; const username = item.split('@')[0]; if (username.length === 0) { return; } } uris.push(item); } }); } if (uris) { this.props.inviteParticipants(uris); this.setState({participants: ''}); } this.props.close(); } isValidUri(uri) { if (uri === this.props.accountId) { return false; } let username = uri.split('@')[0]; let domain = uri.split('@')[1]; if (username.match(/^(\+?)([\-|\d]+)$/) && !domain) { return false; } if (domain) { if (domain.indexOf('yahoo') > -1) { return false; } if (domain.indexOf('icloud') > -1) { return false; } if (domain.indexOf('gmail') > -1) { return false; } } return true; } searchContacts(text) { const search_text = text; let filteredContacts = []; let searching = false; if (!text.startsWith(this.state.participants)) { this.setState({participants: text, filteredContacts: [], searching: false}); return; } if (text.indexOf(',') > -1) { const text_els = text.split(','); text = text_els[text_els.length - 1].trim() } if (text.length > 1) { filteredContacts = this.props.lookupContacts(text); let already_added = this.state.participants.replace(/\s+,\s+/g, ",").split(','); filteredContacts = filteredContacts.filter(x => !already_added.includes(x.uri) && this.isValidUri(x.uri)); if (filteredContacts.length > 0) { searching = true; } } this.setState({filteredContacts: filteredContacts.slice(0, 6), participants: search_text, searching: searching }); } updateParticipants(contact) { let participants = this.state.participants.replace(/\s+,\s+/g, ","); let els = participants.split(','); if (els.length === 1) { participants = contact.uri; } else { els.pop(-1); els.push(contact.uri); participants = els.toString(','); } this.setState({participants: participants.replace(/,/g, ", "), filteredContacts: [], searching: false}); } render() { const showAutocomplete = false; return ( Share web link {showAutocomplete ? i.toString()} onChangeText={(text) => this.searchContacts(text)} placeholder="Enter Sylk accounts separated by ," renderItem={({item}) => ( {this.updateParticipants(item);}}> {item.displayName ? item.displayName + ' ('+ item.uri + ')' : item.uri} )} /> : null} {showAutocomplete ? : null} Select an external application to share the conference web link: ); } } InviteParticipantsModal.propTypes = { notificationCenter : PropTypes.func.isRequired, - show: PropTypes.bool.isRequired, + show: PropTypes.bool, close: PropTypes.func.isRequired, inviteParticipants: PropTypes.func, currentParticipants: PropTypes.array, previousParticipants: PropTypes.array, alreadyInvitedParticipants: PropTypes.array, room: PropTypes.string, defaultDomain: PropTypes.string, accountId: PropTypes.string, lookupContacts: PropTypes.func }; export default InviteParticipantsModal; diff --git a/app/components/NavigationBar.js b/app/components/NavigationBar.js index 0e9b3fa..d9bda81 100644 --- a/app/components/NavigationBar.js +++ b/app/components/NavigationBar.js @@ -1,638 +1,632 @@ import React, { Component } from 'react'; import { Linking, Image, Platform, View } from 'react-native'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { Appbar, Menu, Divider, Text } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import config from '../config'; import styles from '../assets/styles/blink/_NavigationBar.scss'; const blinkLogo = require('../assets/images/blink-white-big.png'); import AboutModal from './AboutModal'; import CallMeMaybeModal from './CallMeMaybeModal'; import EditConferenceModal from './EditConferenceModal'; import AddContactModal from './AddContactModal'; import EditContactModal from './EditContactModal'; import ExportPrivateKeyModal from './ExportPrivateKeyModal'; import DeleteHistoryModal from './DeleteHistoryModal'; import VersionNumber from 'react-native-version-number'; import ShareConferenceLinkModal from './ShareConferenceLinkModal'; class NavigationBar extends Component { constructor(props) { super(props); autoBind(this); let displayName = this.props.selectedContact ? this.props.selectedContact.name : this.props.displayName; let organization = this.props.selectedContact ? this.props.selectedContact.organization : this.props.organization; this.state = { - showAboutModal: false, syncConversations: this.props.syncConversations, inCall: this.props.inCall, showCallMeMaybeModal: this.props.showCallMeMaybeModal, contactsLoaded: this.props.contactsLoaded, appStoreVersion: this.props.appStoreVersion, - showEditContactModal: false, - showEditConferenceModal: false, showExportPrivateKeyModal: this.props.showExportPrivateKeyModal, - showDeleteHistoryModal: false, - showAddContactModal: false, - showConferenceLinkModal: false, privateKeyPassword: null, registrationState: this.props.registrationState, connection: this.props.connection, proximity: this.props.proximity, selectedContact: this.props.selectedContact, mute: false, menuVisible: false, accountId: this.props.accountId, account: this.props.account, displayName: displayName, myDisplayName: this.props.myDisplayName, email: this.props.email, organization: organization, publicKey: this.props.publicKey, showPublicKey: false, messages: this.props.messages, userClosed: false, blockedUris: this.props.blockedUris, ssiRequired: this.props.ssiRequired, filteredMessageIds: this.props.filteredMessageIds, contentTypes: this.props.contentTypes } this.menuRef = React.createRef(); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.account !== null && nextProps.account.id !== this.state.accountId) { this.setState({accountId: nextProps.accountId}); } let displayName = nextProps.selectedContact ? nextProps.selectedContact.name : nextProps.displayName; let organization = nextProps.selectedContact ? nextProps.selectedContact.organization : nextProps.organization; this.setState({registrationState: nextProps.registrationState, connection: nextProps.connection, syncConversations: nextProps.syncConversations, contactsLoaded: nextProps.contactsLoaded, displayName: displayName, myDisplayName: nextProps.myDisplayName, appStoreVersion: nextProps.appStoreVersion, showExportPrivateKeyModal: nextProps.showExportPrivateKeyModal, email: nextProps.email, organization: organization, proximity: nextProps.proximity, account: nextProps.account, userClosed: true, inCall: nextProps.inCall, publicKey: nextProps.publicKey, showDeleteHistoryModal: nextProps.showDeleteHistoryModal, selectedContact: nextProps.selectedContact, messages: nextProps.messages, showCallMeMaybeModal: nextProps.showCallMeMaybeModal, blockedUris: nextProps.blockedUris, ssiRequired: nextProps.ssiRequired, filteredMessageIds: nextProps.filteredMessageIds, contentTypes: nextProps.contentTypes }); if (nextProps.menuVisible) { this.setState({menuVisible: nextProps.menuVisible}); console.log('Next menu visible', nextProps.menuVisible); } } handleMenu(event) { this.callUrl = `${config.publicUrl}/call/${this.state.accountId}`; switch (event) { case 'about': this.toggleAboutModal(); break; case 'callMeMaybe': this.props.toggleCallMeMaybeModal(); break; case 'shareConferenceLinkModal': this.showConferenceLinkModal(); break; case 'displayName': this.toggleEditContactModal(); break; case 'speakerphone': this.props.toggleSpeakerPhone(); break; case 'proximity': this.props.toggleProximity(); break; case 'ssi': this.props.toggleSSIFunc(); break; case 'logOut': this.props.logout(); break; case 'logs': this.props.showLogs(); break; case 'refetchMessages': this.props.refetchMessages(); break; case 'deleteSsiCredential': this.props.deleteSsiCredential(this.state.selectedContact); break; case 'deleteSsiConnection': this.props.deleteSsiConnection(this.state.selectedContact); break; case 'preview': this.props.preview(); break; case 'audio': this.audioCall(); break; case 'video': this.videoCall(); break; case 'resumeTransfers': this.resumeTransfers(); break; case 'conference': this.conferenceCall(); break; case 'addContact': this.toggleAddContactModal(); break; case 'editContact': if (this.state.selectedContact && this.state.selectedContact.uri.indexOf('@videoconference') > -1) { this.setState({showEditConferenceModal: true}); } else { this.setState({showEditContactModal: true}); } break; case 'deleteMessages': this.setState({showDeleteHistoryModal: true}); break; case 'toggleFavorite': this.props.toggleFavorite(this.state.selectedContact.uri); break; case 'toggleBlocked': this.props.toggleBlocked(this.state.selectedContact.uri); break; case 'sendPublicKey': this.props.sendPublicKey(this.state.selectedContact.uri); break; case 'exportPrivateKey': if (this.state.publicKey) { this.showExportPrivateKeyModal(); } else { this.props.showImportModal(true); } break; case 'showPublicKey': this.setState({showEditContactModal: !this.state.showEditContactModal, showPublicKey: true}); break; case 'checkUpdate': if (Platform.OS === 'android') { Linking.openURL('https://play.google.com/store/apps/details?id=com.agprojects.sylk'); } else { Linking.openURL('https://apps.apple.com/us/app/id1489960733'); } break; case 'settings': Linking.openURL(config.serverSettingsUrl); break; default: break; } this.setState({menuVisible: false}); } saveContact(displayName, organization='', email='') { if (!displayName) { return; } if (this.state.selectedContact && this.state.selectedContact.uri !== this.state.accountId) { this.props.saveContact(this.state.selectedContact.uri, displayName, organization); } else { this.setState({displayName: displayName}); this.props.saveContact(this.state.accountId, displayName, organization, email); } } toggleMute() { this.setState(prevState => ({mute: !prevState.mute})); this.props.toggleMute(); } toggleAboutModal() { this.setState({showAboutModal: !this.state.showAboutModal}); } showConferenceLinkModal() { this.setState({showConferenceLinkModal: true}); } hideConferenceLinkModal() { this.setState({showConferenceLinkModal: false}); } audioCall() { let uri = this.state.selectedContact.uri; this.props.startCall(uri, {audio: true, video: false}); } videoCall() { let uri = this.state.selectedContact.uri; this.props.startCall(uri, {audio: true, video: true}); } resumeTransfers() { this.props.resumeTransfers(); } conferenceCall() { this.props.showConferenceModalFunc(); } toggleAddContactModal() { this.setState({showAddContactModal: !this.state.showAddContactModal}); } closeDeleteHistoryModal() { this.setState({showDeleteHistoryModal: false}); } showEditContactModal() { this.setState({showEditContactModal: true, showPublicKey: false}); } hideEditContactModal() { this.setState({showEditContactModal: false, showPublicKey: false, userClosed: true}); } saveConference(room, participants, displayName=null) { this.props.saveConference(room, participants, displayName); this.setState({showEditConferenceModal: false}); } toggleEditContactModal() { if (this.state.showEditContactModal) { this.hideEditContactModal(); } else { this.showEditContactModal(); }; } closeEditConferenceModal() { this.setState({showEditConferenceModal: false}); } showExportPrivateKeyModal() { const password = Math.random().toString().substr(2, 6); this.setState({privateKeyPassword: password}); this.props.showExportPrivateKeyModalFunc() } render() { const muteIcon = this.state.mute ? 'bell-off' : 'bell'; if (this.state.menuVisible && !this.state.appStoreVersion) { this.props.checkVersionFunc() } let subtitleStyle = this.props.isTablet ? styles.tabletSubtitle: styles.subtitle; let titleStyle = this.props.isTablet ? styles.tabletTitle: styles.title; let statusIcon = null; let statusColor = 'green'; let tags = []; statusIcon = 'check-circle'; if (!this.state.connection || this.state.connection.state !== 'ready') { statusIcon = 'alert-circle'; statusColor = 'red'; } else if (this.state.registrationState !== 'registered') { statusIcon = 'alert-circle'; statusColor = 'orange'; } let callUrl = callUrl = config.publicUrl + "/call/" + this.state.accountId; let subtitle = 'Signed in as ' + this.state.accountId; let proximityTitle = this.state.proximity ? 'No proximity sensor' : 'Proximity sensor'; let proximityIcon = this.state.proximity ? 'ear-hearing-off' : 'ear-hearing'; let isConference = false; let hasMessages = true; // allow user to select this after local messages were removed, to delete them remotely if (this.state.selectedContact) { if (Object.keys(this.state.messages).indexOf(this.state.selectedContact.uri) > -1 && this.state.messages[this.state.selectedContact.uri].length > 0) { hasMessages = true; } tags = this.state.selectedContact.tags; isConference = this.state.selectedContact.conference || tags.indexOf('conference') > -1; } let favoriteTitle = (this.state.selectedContact && tags && tags.indexOf('favorite') > -1) ? 'Unfavorite' : 'Favorite'; let favoriteIcon = (this.state.selectedContact && tags && tags.indexOf('favorite') > -1) ? 'flag-minus' : 'flag'; let extraMenu = false; let importKeyLabel = this.state.publicKey ? "Export private key...": "Import private key..."; let showEditModal = false; if (this.state.selectedContact) { showEditModal = this.state.showEditContactModal && !this.state.syncConversations; } else { showEditModal = !this.state.syncConversations && this.state.contactsLoaded && (this.state.showEditContactModal || (!this.state.displayName && this.state.publicKey !== null && !this.state.userClosed)) || false; } let hasUpdate = this.state.appStoreVersion && this.state.appStoreVersion.version > VersionNumber.appVersion; let updateTitle = hasUpdate ? 'Update Sylk...' : 'Check for updates...'; let isAnonymous = this.state.selectedContact && (this.state.selectedContact.uri.indexOf('@guest.') > -1 || this.state.selectedContact.uri.indexOf('anonymous@') > -1); let canCall = !isConference && !this.state.inCall && !isAnonymous && tags.indexOf('ssi') === -1; let blockedTitle = (this.state.selectedContact && tags && tags.indexOf('blocked') > -1) ? 'Unblock' : isAnonymous ? 'Block anonymous callers': 'Block'; if (isAnonymous && this.state.blockedUris.indexOf('anonymous@anonymous.invalid') > -1) { blockedTitle = 'Allow anonymous callers'; } let ssiTitle = this.state.ssiRequired ? 'Disable SSI' : 'Enable SSI'; return ( {this.state.selectedContact? {this.props.goBackFunc()}} /> : } {this.props.isTablet? {subtitle} : null} {statusColor == 'green' ? : null } { this.state.selectedContact ? this.setState({menuVisible: !this.state.menuVisible})} anchor={ this.setState({menuVisible: !this.state.menuVisible})} /> } > {canCall ? this.handleMenu('audio')} icon="phone" title="Audio call"/> :null} {canCall ? this.handleMenu('video')} icon="video" title="Video call"/> :null} {!this.state.inCall && isConference ? this.handleMenu('conference')} icon="account-group" title="Join conference..."/> :null} {!this.state.inCall && isConference ? this.handleMenu('shareConferenceLinkModal')} icon="share-variant" title="Share web link..."/> :null} { hasMessages && !this.state.inCall && tags.indexOf('ssi') === -1 ? this.handleMenu('deleteMessages')} icon="delete" title="Delete messages..."/> : null } { hasMessages && !this.state.inCall && tags.indexOf('ssi') === -1 && 'paused' in this.state.contentTypes ? this.handleMenu('resumeTransfers')} icon="delete" title="Resume transfers"/> : null } { hasMessages && tags.indexOf('test') === -1 && !isConference && false? this.handleMenu('sendPublicKey')} icon="key-change" title="Send my public key..."/> : null} {this.props.publicKey && false? this.handleMenu('showPublicKey')} icon="key-variant" title="Show public key..."/> : null} {tags.indexOf('test') === -1 && !this.state.inCall && !isAnonymous && tags.indexOf('ssi') === -1 ? this.handleMenu('toggleFavorite')} icon={favoriteIcon} title={favoriteTitle}/> : null} {tags.indexOf('test') === -1 && tags.indexOf('favorite') === -1 && !this.state.inCall && tags.indexOf('ssi') === -1 ? this.handleMenu('toggleBlocked')} icon="block-helper" title={blockedTitle}/> : null} {!this.state.inCall && !hasMessages && tags.indexOf('test') === -1 && tags.indexOf('ssi') === -1? this.handleMenu('deleteMessages')} icon="delete" title="Delete contact..."/> : null} {tags.indexOf('ssi') === -1 ? this.handleMenu('editContact')} icon="account" title="Edit contact..."/> : null} {this.state.selectedContact && tags.indexOf('ssi-credential') > -1? this.handleMenu('deleteSsiCredential')} icon="delete" title="Delete SSI credential..."/> : null} {this.state.selectedContact && tags.indexOf('ssi-connection') > -1 && tags.indexOf('readonly') === -1 ? this.handleMenu('deleteSsiConnection')} icon="delete" title="Delete SSI connection..."/> : null} : this.setState({menuVisible: !this.state.menuVisible})} anchor={ this.setState({menuVisible: !this.state.menuVisible})} /> } > this.handleMenu('callMeMaybe')} icon="share" title="Call me, maybe?" /> {!this.state.syncConversations && !this.state.inCall ? this.handleMenu('displayName')} icon="rename-box" title="My account..." /> : null} this.handleMenu('addContact')} icon="account-plus" title="Add contact..."/> {!this.state.inCall ? this.handleMenu('conference')} icon="account-group" title="Join conference..."/> :null} {!this.state.inCall && false ? this.handleMenu('preview')} icon="video" title="Video preview" />:null} {!this.state.inCall ? this.handleMenu('exportPrivateKey')} icon="key" title={importKeyLabel} />:null} {false ? this.handleMenu('checkUpdate')} icon="update" title={updateTitle} /> :null} {!this.state.inCall ? this.handleMenu('deleteMessages')} icon="delete" title="Wipe device..."/> :null} {!this.state.inCall && __DEV__ ? this.handleMenu('refetchMessages')} icon="delete" title="Refetch messages (DEV)"/> :null} {extraMenu ? this.handleMenu('settings')} icon="wrench" title="Server settings..." /> this.handleMenu('proximity')} icon={proximityIcon} title={proximityTitle} /> : null} this.handleMenu('ssi')} icon="key" title={ssiTitle}/> this.handleMenu('logs')} icon="timeline-text-outline" title="Logs" /> {!this.state.inCall ? this.handleMenu('about')} icon="information" title="About Sylk"/> : null} {!this.state.inCall ? this.handleMenu('logOut')} icon="logout" title="Sign out" /> : null} } ); } } NavigationBar.propTypes = { notificationCenter : PropTypes.func.isRequired, logout : PropTypes.func.isRequired, preview : PropTypes.func.isRequired, toggleSpeakerPhone : PropTypes.func.isRequired, toggleProximity : PropTypes.func.isRequired, showLogs : PropTypes.func.isRequired, inCall : PropTypes.bool, contactsLoaded : PropTypes.bool, proximity : PropTypes.bool, displayName : PropTypes.string, myDisplayName : PropTypes.string, email : PropTypes.string, organization : PropTypes.string, account : PropTypes.object, accountId : PropTypes.string, connection : PropTypes.object, toggleMute : PropTypes.func, orientation : PropTypes.string, isTablet : PropTypes.bool, selectedContact : PropTypes.object, goBackFunc : PropTypes.func, replicateKey : PropTypes.func, publicKeyHash : PropTypes.string, publicKey : PropTypes.string, deleteMessages : PropTypes.func, toggleBlocked : PropTypes.func, toggleFavorite : PropTypes.func, saveConference : PropTypes.func, defaultDomain : PropTypes.string, favoriteUris : PropTypes.array, startCall : PropTypes.func, startConference : PropTypes.func, saveContact : PropTypes.func, addContact : PropTypes.func, deleteContact : PropTypes.func, removeContact : PropTypes.func, deletePublicKey : PropTypes.func, sendPublicKey : PropTypes.func, messages : PropTypes.object, showImportModal : PropTypes.func, syncConversations : PropTypes.bool, showCallMeMaybeModal: PropTypes.bool, toggleCallMeMaybeModal : PropTypes.func, showConferenceModalFunc : PropTypes.func, appStoreVersion : PropTypes.object, checkVersionFunc: PropTypes.func, toggleSSIFunc: PropTypes.func, ssiRequired: PropTypes.bool, refetchMessages: PropTypes.func, showExportPrivateKeyModal: PropTypes.bool, showExportPrivateKeyModalFunc: PropTypes.func, hideExportPrivateKeyModalFunc: PropTypes.func, blockedUris: PropTypes.array, myuuid: PropTypes.string, deleteSsiCredential: PropTypes.func, resumeTransfers: PropTypes.func, deleteSsiConnection: PropTypes.func, filteredMessageIds: PropTypes.array, contentTypes: PropTypes.object }; export default NavigationBar; diff --git a/app/components/ShareConferenceLinkModal.js b/app/components/ShareConferenceLinkModal.js index 8827529..3f18f72 100644 --- a/app/components/ShareConferenceLinkModal.js +++ b/app/components/ShareConferenceLinkModal.js @@ -1,109 +1,109 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View, TouchableOpacity } from 'react-native'; import { Dialog, Portal, Text, Button, Surface, TextInput, IconButton} from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import { openComposer } from 'react-native-email-link'; import Share from 'react-native-share'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import utils from '../utils'; import config from '../config'; import styles from '../assets/styles/blink/_ConferenceModal.scss'; class ShareConferenceLinkModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { roomUrl: config.publicUrl + '/conference/' + this.props.room.split('@')[0] } } handleClipboardButton(event) { utils.copyToClipboard(this.state.roomUrl); this.props.notificationCenter().postSystemNotification('Conference', {body: 'address copied to clipboard'}); this.props.close(); } handleEmailButton(event) { const emailMessage = 'You can join my conference at ' + this.state.roomUrl; const subject = 'Join conference, maybe?'; openComposer({ subject, body: emailMessage }) this.props.close(); } handleShareButton(event) { const subject = 'Join conference, maybe?'; const message = 'You can join my conference at ' + this.state.roomUrl; let options= { subject: subject, message: message } Share.open(options) .then((res) => { this.props.close(); }) .catch((err) => { this.props.close(); }); } render() { return ( Share web link {this.state.roomUrl} Select an external application to share the conference web link: ); } } ShareConferenceLinkModal.propTypes = { notificationCenter : PropTypes.func.isRequired, - show: PropTypes.bool.isRequired, + show: PropTypes.bool, close: PropTypes.func.isRequired, room: PropTypes.string }; export default ShareConferenceLinkModal; diff --git a/app/components/ShareMessageModal.js b/app/components/ShareMessageModal.js index 6fe0b04..d9a3a0b 100644 --- a/app/components/ShareMessageModal.js +++ b/app/components/ShareMessageModal.js @@ -1,155 +1,155 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View } from 'react-native'; import { Dialog, Portal, Text, Surface, IconButton} from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; import { openComposer } from 'react-native-email-link'; import Share from 'react-native-share'; const RNFS = require('react-native-fs'); //var Mailer = require('NativeModules').RNMail; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import utils from '../utils'; import styles from '../assets/styles/blink/_ConferenceModal.scss'; class ShareMessageModal extends Component { constructor(props) { super(props); autoBind(this); this.state = { message: props.message, show: props.show } } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({message: nextProps.message, show: nextProps.show }); } handleClipboardButton(event) { utils.copyToClipboard(this.state.message.text); this.props.close(); } handleEmailButton(event) { const emailMessage = this.state.message.text; const subject = 'Share Sylk message'; /* let mailMessage = { subject: subject, body: emailMessage }; if (this.state.message.metadata) { local_url = this.state.message.metadata.local_url; mailMessage.attachment = { path: this.state.message.metadata.local_url, // The absolute path of the file from which to read data. type: this.state.message.metadata.filetype, // Mime Type: jpg, png, doc, ppt, html, pdf name: this.state.message.metadata.filename // Optional: Custom filename for attachment } } Mailer.mail(mailMessage, (error, event) => { if (error) { console.log('Error', error); } }); */ openComposer({ subject, body: emailMessage }) this.props.close(); } async handleShareButton(event) { let local_url; let what = 'message'; if (this.state.message.metadata) { local_url = this.state.message.metadata.local_url; if (this.state.message.image) { what = 'photo'; let res = await RNFS.readFile(local_url, 'base64'); local_url = `data:${this.state.message.metadata.filetype};base64,${res}`; } else if (utils.isAudio(this.state.message.metadata.filename)) { what = 'Audio message'; local_url = Platform.OS === 'ios' ? local_url : 'file://' + local_url; } else if (this.state.message.metadata.video) { what = 'Video'; local_url = Platform.OS === 'ios' ? local_url : 'file://' + local_url; } else { local_url = Platform.OS === 'ios' ? local_url : 'file://' + local_url; } } let options= { title: 'Share via', subject: 'Share ' + what, message: this.state.message.text, url: local_url } if (this.state.message.metadata) { options.type = this.state.message.metadata.filetype; } console.log('Sharing data...'); Share.open(options) .then((res) => { this.props.close(); }) .catch((err) => { this.props.close(); }); } render() { return ( Share message ); } } ShareMessageModal.propTypes = { - show: PropTypes.bool.isRequired, + show: PropTypes.bool, close: PropTypes.func.isRequired, message: PropTypes.object }; export default ShareMessageModal;