diff --git a/app/app.js b/app/app.js index 026f8f5..c7a6d27 100644 --- a/app/app.js +++ b/app/app.js @@ -1,7812 +1,7990 @@ // copyright AG Projects 2020-2021 import React, { Component, Fragment } from 'react'; import { Alert, View, SafeAreaView, ImageBackground, AppState, Linking, Platform, StyleSheet, Vibration, PermissionsAndroid} from 'react-native'; import { DeviceEventEmitter, BackHandler } from 'react-native'; import { Provider as PaperProvider, DefaultTheme } from 'react-native-paper'; import { registerGlobals } from 'react-native-webrtc'; import { Router, Route, Link, Switch } from 'react-router-native'; import history from './history'; import Logger from "../Logger"; import autoBind from 'auto-bind'; import { firebase } from '@react-native-firebase/messaging'; import VoipPushNotification from 'react-native-voip-push-notification'; import uuid from 'react-native-uuid'; import { getUniqueId, getBundleId, isTablet, getPhoneNumber} from 'react-native-device-info'; import RNDrawOverlay from 'react-native-draw-overlay'; import PushNotificationIOS from "@react-native-community/push-notification-ios"; import PushNotification , {Importance} from "react-native-push-notification"; import Contacts from 'react-native-contacts'; import BackgroundTimer from 'react-native-background-timer'; import DeepLinking from 'react-native-deep-linking'; import base64 from 'react-native-base64'; import SoundPlayer from 'react-native-sound-player'; import RNSimpleCrypto from "react-native-simple-crypto"; import OpenPGP from "react-native-fast-openpgp"; import ShortcutBadge from 'react-native-shortcut-badge'; import { getAppstoreAppMetadata } from "react-native-appstore-version-checker"; //import ReceiveSharingIntent from 'react-native-receive-sharing-intent'; import {Keyboard} from 'react-native'; registerGlobals(); import * as sylkrtc from 'react-native-sylkrtc'; import InCallManager from 'react-native-incall-manager'; import RNCallKeep, { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep'; import RegisterBox from './components/RegisterBox'; import ReadyBox from './components/ReadyBox'; import Call from './components/Call'; import Conference from './components/Conference'; import FooterBox from './components/FooterBox'; import StatusBox from './components/StatusBox'; import ImportPrivateKeyModal from './components/ImportPrivateKeyModal'; import IncomingCallModal from './components/IncomingCallModal'; import LogsModal from './components/LogsModal'; import NotificationCenter from './components/NotificationCenter'; import LoadingScreen from './components/LoadingScreen'; import NavigationBar from './components/NavigationBar'; import Preview from './components/Preview'; import CallManager from './CallManager'; import SQLite from 'react-native-sqlite-storage'; //SQLite.DEBUG(true); SQLite.enablePromise(true); import xtype from 'xtypejs'; import xss from 'xss'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import momenttz from 'moment-timezone'; import utils from './utils'; import config from './config'; import storage from './storage'; 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 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; }; } })(); const requestCameraPermission = async () => { if (Platform.OS !== 'android') { return; } try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.CAMERA, { title: "Sylk camera permission", message: "Sylk needs access to your camera " + "so you can have video chat.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { //console.log("You can use the camera"); } else { console.log("Camera permission denied"); } } catch (err) { console.warn(err); } try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: "Sylk microphone permission", message: "Sylk needs access to your microphone " + "so you can have audio calls.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { //console.log("You can use the microphone"); } else { console.log("Microphone permission denied"); } } catch (err) { console.warn(err); } }; class Sylk extends Component { constructor() { super(); autoBind(this) this._loaded = false; let isFocus = Platform.OS === 'ios'; this._initialState = { appState: null, autoLogin: true, inFocus: isFocus, accountId: '', password: '', displayName: '', email: '', organization: '', account: null, lastSyncId: null, accountVerified: false, registrationState: null, registrationKeepalive: false, incomingCall: null, currentCall: null, connection: null, showIncomingModal: false, showScreenSharingModal: false, status: null, targetUri: '', missedTargetUri: '', loading: null, syncConversations: false, localMedia: null, generatedVideoTrack: false, contacts: [], devices: {}, speakerPhoneEnabled: null, orientation : 'portrait', Height_Layout : '', Width_Layout : '', outgoingCallUUID: null, incomingCallUUID: null, 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, 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, serverPublicKey: null, generatingKey: false, appStoreVersion: null, firstSyncDone: false, showExportPrivateKeyModal: false, serverQueriedForPublicKey: false, navigationItems: {today: false, yesterday: false, conference: false} }; utils.timestampedLog('Init app'); this.pendingNewSQLMessages = []; this.newSyncMessagesCount = 0; this.syncStartTimestamp = null; this.syncRequested = false; this.mustSendPublicKey = false; 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.sync_pending_items = []; this.signup = {}; this.last_signup = 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.msg_sound_played_ts = null; this.initialChatContact = null; this.serverPublicKey = null; storage.initialize(); this.callKeeper = new CallManager(RNCallKeep, 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') { InCallManager.requestRecordPermission() .then((requestedRecordPermissionResult) => { //console.log("InCallManager.requestRecordPermission() requestedRecordPermissionResult: ", requestedRecordPermissionResult); }) .catch((err) => { //console.log("InCallManager.requestRecordPermission() catch: ", err); }); } requestCameraPermission(); // Load camera/mic preferences storage.get('devices').then((devices) => { if (devices) { this.setState({devices: devices}); } }); storage.get('account').then((account) => { if (account) { this.setState({accountVerified: account.verified}); } }); storage.get('keys').then((keys) => { if (keys) { const public_key = keys.public.replace(/\r/g,''); const private_key = keys.private.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; this.setState({keys: keys}); console.log("Loaded PGP public key"); } }).catch((err) => { console.log("PGP keys loading error:", err); }); storage.get('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': 5, 'contacts': 7, 'keys': 2} 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']}], }, '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: []}]} }; this.db = null; this.initSQL(); } async saveMyKey(keys) { this.setState({keys: {private: keys.private, public: keys.public}}); let myContacts = this.state.myContacts; if (this.state.account) { this.state.account.syncConversations(); var uri = uuid.v4() + '@' + this.state.defaultDomain; console.log('Send 1st public to', uri); this.sendPublicKey(uri); 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); let params = [this.state.accountId, keys.private, keys.public, unixTime]; await this.ExecuteQuery("INSERT INTO keys (account, private_key, public_key, timestamp) VALUES (?, ?, ?, ?)", params).then((result) => { console.log('SQL inserted private key'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') > -1) { this.updateKeySql(keys); } else { console.log('Save keys SQL error:', error); } }); } async saveLastSyncId(id, force=false) { if (!force) { if (!this.state.keys || !this.state.keys.private) { console.log('Skip saving last sync id until we have a private key'); return } if (!this.state.firstSyncDone) { console.log('Skip saving last sync id until first sync is done'); return } } let params = [id, this.state.accountId]; await this.ExecuteQuery("update keys set last_sync_id = ? where account = ?", params).then((result) => { console.log('SQL saved last sync id', id); this.setState({lastSyncId: id}); }).catch((error) => { console.log('Save last sync id SQL error:', error); }); } async updateKeySql(keys) { let current_datetime = new Date(); const unixTime = Math.floor(current_datetime / 1000); let params = [keys.private, keys.public, unixTime, this.state.accountId]; await this.ExecuteQuery("update keys set private_key = ?, public_key = ?, timestamp = ? where account = ?", params).then((result) => { console.log('SQL updated private key'); }).catch((error) => { console.log('SQL error:', error); }); } loadMyKeys() { console.log('Loading PGP keys...'); let keys = {}; let lastSyncId; 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); keys.public = item.public_key; if (this.serverPublicKey === item.public_key) { this.setState({showImportPrivateKeyModal: false}); } keys.private = item.private_key; console.log('Loaded PGP private key for account', this.state.accountId); if (!item.last_sync_id && this.lastSyncedMessageId) { this.setState({keys: keys}); this.saveLastSyncId(this.lastSyncedMessageId); console.log('Migrated last sync id to SQL database'); storage.remove('lastSyncedMessageId'); lastSyncId = this.lastSyncedMessageId; } else { if (item.last_sync_id) { console.log('Loaded last sync id', item.last_sync_id); } this.setState({keys: keys, lastSyncId: item.last_sync_id}); lastSyncId = item.last_sync_id; } if (this.state.registrationState ==='registered' && !this.syncRequested) { this.syncRequested = true; console.log('Request sync messages from server', lastSyncId); this.state.account.syncConversations(lastSyncId); } } else { if (this.state.account) { this.generateKeysIfNecessary(this.state.account); } else { console.log('Wait for account become active...'); } } }); this.setState({contactsLoaded: true}); } async generateKeys() { const Options = { comment: 'Sylk key', email: this.state.accountId, name: this.state.displayName || this.state.accountId, keyOptions: KeyOptions } console.log('Generating key pair with options', Options); this.setState({loading: 'Generating private key...', generatingKey: true}); await OpenPGP.generate(Options).then((keys) => { const public_key = keys.publicKey.replace(/\r/g, '').trim(); const private_key = keys.privateKey.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; console.log("PGP keypair generated"); this.setState({loading: null, generatingKey: false}); this.setState({showImportPrivateKeyModal: false}); this.saveMyKey(keys); this.showCallMeModal(); }).catch((error) => { console.log("PGP keys generation error:", error); }); } resetStorage() { return; console.log('Reset storage'); this.ExecuteQuery('delete from contacts'); this.ExecuteQuery('delete from messages'); this.saveLastSyncId(null); } loadSylkContacts() { console.log('Loading contacts...') let myContacts = {}; let blockedUris = []; let favoriteUris = []; let missedCalls = []; let myInvitedParties = {}; let localTime; let email; let contact; + let timestamp; 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; - this.sql_contacts_keys.push(item.uri); - 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 = new Date(item.timestamp * 1000); + 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, 'with name', item.name); + 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 account', this.state.accountId); + 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]); } 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 ma,e'); } } }); } 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, \ 'content' BLOB, \ 'content_type' 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, \ 'encrypted' INTEGER default 0, \ 'direction' TEXT, \ PRIMARY KEY (account, msg_id)) \ "; this.ExecuteQuery(create_table_messages).then((success) => { //console.log('SQL messages table OK'); }).catch((error) => { console.log(create_table_messages); console.log('SQL messages table creation error:', error); }); let create_table_contacts = "CREATE TABLE IF NOT EXISTS 'contacts' ( \ 'uri' TEXT, \ 'account' TEXT, \ 'name' TEXT, \ 'organization' TEXT, \ 'tags' TEXT, \ 'photo' BLOB, \ 'email' TEXT, \ 'participants' TEXT, \ 'public_key' TEXT, \ 'timestamp' INTEGER, \ 'direction' TEXT, \ 'last_message' TEXT, \ 'last_message_id' TEXT, \ 'unread_messages' TEXT, \ 'last_call_media' TEXT, \ 'last_call_duration' INTEGER default 0, \ 'last_call_id' TEXT, \ 'conference' INTEGER default 0, \ PRIMARY KEY (account, uri)) \ "; this.ExecuteQuery(create_table_contacts).then((success) => { //console.log('SQL contacts table OK'); }).catch((error) => { console.log(create_table_contacts); console.log('SQL messages table creation error:', error); }); let create_table_keys = "CREATE TABLE IF NOT EXISTS 'keys' ( \ 'account' TEXT PRIMARY KEY, \ 'private_key' TEXT, \ 'checksum' TEXT, \ 'public_key' TEXT, \ 'last_sync_id' TEXT, \ 'timestamp' INTEGER ) \ "; 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); + //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); + //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); this.db.transaction((trans) => { trans.executeSql(sql, params, (trans, results) => { resolve(results); }, (error) => { reject(error); }); }); }); async loadDeviceContacts() { Contacts.checkPermission((err, permission) => { if (permission === Contacts.PERMISSION_UNDEFINED) { Contacts.requestPermission((err, requestedContactsPermissionResult) => { if (err) { console.log("Contacts.requestPermission()catch: ", err); } console.log("Contacts.requestPermission() requestPermission: ", requestedContactsPermissionResult); }) } }) Contacts.getAll((err, contacts) => { if (err === 'denied'){ console.log('Access to contacts denied') } else { // 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() { if(this.state.Width_Layout > this.state.Height_Layout && this.state.orientation !== 'landscape') { this.setState({orientation: 'landscape'}); } else { this.setState({orientation: 'portrait'}); } } changeRoute(route, reason) { utils.timestampedLog('Change route', route, 'with reason:', reason); if (this.currentRoute === route) { if (route === '/ready' && this.state.selectedContact) { this.setState({ selectedContact: null, targetUri: '', messages: {}, messageZoomFactor: 1 }); } return; } else { if (route === '/ready' && this.state.selectedContact) { this.getMessages(this.state.selectedContact.uri); } } if (this.currentRoute !== route) { utils.timestampedLog('Change route:', this.currentRoute, '->', route, reason); } 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; } this.startedByPush = false; if (this.state.currentCall && reason === 'outgoing_connection_failed' && this.state.currentCall.direction === 'outgoing') { console.log('Connection failed this.state.currentCall', this.state.currentCall); 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 { this.setState({ outgoingCallUUID: null, currentCall: null, callContact: null, + 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') { const uri = `${utils.generateSillyName()}@${config.defaultConferenceDomain}`; const options = {audio: this.outgoingMedia ? this.outgoingMedia.audio: true, video: this.outgoingMedia ? this.outgoingMedia.video: true, participants: this.participantsToInvite} this.callKeepStartConference(uri.toLowerCase(), options); } else { if (this.state.account && this._loaded) { setTimeout(() => { this.updateServerHistory() }, 1500); } } } if (reason === 'registered') { setTimeout(() => { this.updateServerHistory() }, 1500); } if (reason === 'no_more_calls') { this.updateServerHistory() } if (reason === 'start_up') { storage.get('account').then((account) => { if (account) { this.handleRegistration(account.accountId, account.password); } else { this.changeRoute('/login', 'start up'); } }); this.fetchSharedItems(); } } this.currentRoute = route; history.push(route); } componentWillUnmount() { utils.timestampedLog('App will unmount'); AppState.removeEventListener('change', this._handleAppStateChange); this._onFinishedPlayingSubscription.remove(); this._onFinishedLoadingSubscription.remove(); this._onFinishedLoadingURLSubscription.remove(); this._onFinishedLoadingFileSubscription.remove(); this.callKeeper.destroy(); this.closeConnection(); this._loaded = false; } get unmounted() { return !this._loaded; } isUnmounted() { return this.unmounted; } backPressed() { console.log('Back button pressed in route', this.currentRoute); if (this.currentRoute === '/ready' && this.state.selectedContact) { this.goBackToHome(); } if (this.currentRoute === '/call' || this.currentRoute === '/conference') { let call = this.state.currentCall || this.state.incomingCall; if (call && call.id) { this.hangupCall(call.id, 'user_hangup_call'); } } return true; } async componentDidMount() { utils.timestampedLog('App did mount'); 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.loadDeviceContacts(); }); this.listenforPushNotifications(); this.listenforSoundNotifications(); this._loaded = true; this.checkVersion(); } checkVersion() { let appId = Platform.OS === 'android' ? "com.agprojects.sylk" : "1489960733"; if (Platform.OS === 'android') { // TODO fix me return; } 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) }) } registerAndroidNotifications(parent) { // Must be outside of any component LifeCycle (such as `componentDidMount`). //console.log('registerAndroidNotifications'); 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) { parent.handleAndroidNotification(notification); // process the 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. ); //console.log('Available Sylk channels:'); PushNotification.getChannels(function (channel_ids) { //console.log(channel_ids); // ['channel_id_1'] }); } handleAndroidNotification(notification) { - //console.log("Handle Android push notification:", notification); + // when user touches the system notification and app launches... + console.log("Handle Android push notification:", notification); let uri = notification.data.from_uri; if (!uri) { return; } if (uri in this.state.myContacts) { if (!this.state.selectedContact) { this.selectContact(this.state.myContacts[uri]); } this.initialChatContact = null; } else { this.initialChatContact = uri; } } + handleiOSNotification(notification) { + // when user touches the system notification and app launches... + console.log("Handle iOS push notification:", notification); + } + sendLocalAndroidNotification(uri, content) { //https://www.npmjs.com/package/react-native-push-notification PushNotification.localNotification({ /* Android Only Properties */ channelId: "sylk-messages", // (required) channelId, if the channel doesn't exist, notification will not trigger. showWhen: true, // (optional) default: true autoCancel: true, // (optional) default: true largeIcon: "ic_launcher", // (optional) default: "ic_launcher". Use "" for no large icon. largeIconUrl: "https://icanblink.com/apple-touch-icon-180x180.png", // (optional) default: undefined smallIcon: "", // (optional) default: "ic_notification" with fallback for "ic_launcher". Use "" for default small icon. bigText: content, // (optional) default: "message" prop subText: "New message", // (optional) default: none //bigPictureUrl: "https://www.example.tld/picture.jpg", // (optional) default: undefined bigLargeIcon: "ic_launcher", // (optional) default: undefined bigLargeIconUrl: "https://www.example.tld/bigicon.jpg", // (optional) default: undefined color: "red", // (optional) default: system default vibrate: true, // (optional) default: true vibration: 100, // vibration length in milliseconds, ignored if vibrate=false, default: 1000 priority: "high", // (optional) set notification priority, default: high ignoreInForeground: true, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting onlyAlertOnce: true, // (optional) alert will open only once with sound and notify, default: false invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true /* iOS and Android properties */ id: 0, // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID title: uri, // (optional) message: content, // (required) //picture: "https://www.example.tld/picture.jpg", // (optional) Display an picture with the notification, alias of `bigPictureUrl` for Android. default: undefined userInfo: {}, // (optional) default: {} (using null throws a JSON value '' error) playSound: false, // (optional) default: true soundName: "default", // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played) number: 10, // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero) repeatType: "day", // (optional) Repeating interval. Check 'Repeating Notifications' section for more info. }); } listenforPushNotifications() { if (this.state.appState === null) { this.setState({appState: 'active'}); } else { return; } if (Platform.OS === 'android') { Linking.getInitialURL().then((url) => { if (url) { utils.timestampedLog('Initial external URL: ' + url); this.eventFromUrl(url); } if (this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } else { this.changeRoute('/login', 'start_up'); } }).catch(err => { logger.error({ err }, 'Error getting external URL'); }); firebase.messaging().getToken() .then(fcmToken => { if (fcmToken) { this._onPushRegistered(fcmToken); } }); Linking.addEventListener('url', this.updateLinkingURL); } else if (Platform.OS === 'ios') { if (this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } else { this.changeRoute('/login', 'start_up'); } VoipPushNotification.addEventListener('register', this._boundOnPushkitRegistered); VoipPushNotification.registerVoipToken(); PushNotificationIOS.addEventListener('register', this._boundOnPushRegistered); PushNotificationIOS.addEventListener('localNotification', this.onLocalNotification); PushNotificationIOS.addEventListener('notification', this.onRemoteNotification); //let permissions = await checkIosPermissions(); //if (!permissions.alert) { PushNotificationIOS.requestPermissions(); //} } this.boundProximityDetect = this._proximityDetect.bind(this); DeviceEventEmitter.addListener('Proximity', this.boundProximityDetect); AppState.addEventListener('change', this._handleAppStateChange); if (Platform.OS === 'ios') { this._boundOnNotificationReceivedBackground = this._onNotificationReceivedBackground.bind(this); this._boundOnLocalNotificationReceivedBackground = this._onLocalNotificationReceivedBackground.bind(this); VoipPushNotification.addEventListener('notification', this._boundOnNotificationReceivedBackground); VoipPushNotification.addEventListener('localNotification', this._boundOnLocalNotificationReceivedBackground); } else if (Platform.OS === 'android') { this.registerAndroidNotifications(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() let event = message.data.event; const callUUID = message.data['session-id']; const from = message.data['from_uri']; const to = message.data['to_uri']; const displayName = message.data['from_display_name']; const outgoingMedia = {audio: true, video: message.data['media-type'] === 'video'}; const mediaType = message.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.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); + if (!from) { + return; + } 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); - if (this.state.selectedContact && this.state.selectedContact.uri !== from) { - this._notificationCenter.postSystemNotification('New message from ' + from); + if (!from) { + return; } + 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; } 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); this.startedByPush = false; if (this.startedByPush) { this.changeRoute('/ready', 'incoming_call_cancelled'); } } 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) { 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:', token); account.setDeviceToken(token, Platform.OS, deviceId, true, bundleId); this.tokenSent = true; } } _handleAndroidFocus = nextFocus => { //utils.timestampedLog('----- APP in focus'); if (Platform.OS === 'ios') { PushNotificationIOS.cancelLocalNotifications(); } else { PushNotification.cancelAllLocalNotifications(); } this.setState({inFocus: true}); this.refreshNavigationItems(); this.fetchSharedItems(); this.respawnConnection(); } fetchSharedItems() { return; console.log('fetchSharedItems...'); ReceiveSharingIntent.getReceivedFiles(files => { // files returns as JSON Array example //[{ filePath: null, text: null, weblink: null, mimeType: null, contentUri: null, fileName: null, extension: null }] console.log(files); if (files.length > 0) { console.log('Will share to contacts', files); this.setState({shareToContacts: true, shareContent: files}); let item = files[0]; let what = 'Share text with contacts'; if (item.weblink) { what = 'Share web link with contacts'; } if (item.filePath) { what = 'Share file with contacts'; } this._notificationCenter.postSystemNotification(what); } else { console.log('Nothing to share'); } }, (error) =>{ console.log(error); }, 'ShareMedia' // share url protocol (must be unique to your app, suggest using your apple bundle id) ); } refreshNavigationItems() { var todayStart = new Date(); todayStart.setHours(0,0,0,0); var yesterdayStart = new Date(); yesterdayStart.setDate(yesterdayStart.getDate() - 2); yesterdayStart.setHours(0,0,0,0); let today = false; let yesterday = false; let conference = false; let navigationItems = this.state.navigationItems; Object.keys(this.state.myContacts).forEach((key) => { if (this.state.myContacts[key].tags.indexOf('conference') > -1 || this.state.myContacts[key].conference) { conference = true; } if (this.state.myContacts[key].timestamp > todayStart) { today = true; } if (this.state.myContacts[key].timestamp > yesterdayStart && this.state.myContacts[key].timestamp < todayStart) { yesterday = true; } }); navigationItems = {today: today, yesterday: yesterday, conference: conference}; this.setState({navigationItems: navigationItems}); } _handleAndroidBlur = nextBlur => { //utils.timestampedLog('----- APP out of focus'); this.setState({inFocus: false}); } _handleAppStateChange = nextAppState => { //utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (nextAppState === this.state.appState) { return; } if (this.callKeeper.countCalls === 0 && !this.state.outgoingCallUUID) { /* utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (this.callKeeper.countCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countCalls, 'calls'); } if (this.callKeeper.countPushCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countPushCalls, 'push calls'); } if (this.startedByPush) { utils.timestampedLog('- APP state changed, started by push in', nextAppState, 'state'); } if (this.state.connection) { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState, 'with connection', Object.id(this.state.connection)); } else { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState); } */ } if (this.state.appState === 'background' && nextAppState === 'active') { this.respawnConnection(nextAppState); } this.setState({appState: nextAppState}); } respawnConnection(state) { if (!this.state.connection) { utils.timestampedLog('Web socket does not exist'); } else if (!this.state.connection.state) { utils.timestampedLog('Web socket is waiting for connection...'); } else { /* if (this.state.connection.state !== 'ready' && this.state.connection.state !== 'connecting') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'reconnecting because', this.state.connection.state); this.state.connection.reconnect(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'new state is', this.state.connection.state); } */ } if (this.state.account) { if (!this.state.connection) { utils.timestampedLog('Active account without connection removed'); this.setState({account: null}); } } else { utils.timestampedLog('No active account'); } if (this.state.accountId && (!this.state.connection || !this.state.account)) { this.handleRegistration(this.state.accountId, this.state.password); } } closeConnection(reason='unmount') { if (!this.state.connection) { return; } if (!this.state.account && this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.connection.close(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.setState({connection: null, account: null}); } else if (this.state.connection && this.state.account) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.account.removeListener('outgoingCall', this.outgoingCall); this.state.account.removeListener('conferenceCall', this.outgoingConference); this.state.account.removeListener('incomingCall', this.incomingCallFromWebSocket); this.state.account.removeListener('missedCall', this.missedCall); this.state.account.removeListener('conferenceInvite', this.conferenceInviteFromWebSocket); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { utils.timestampedLog('Failed to remove account:', error); } else { //utils.timestampedLog('Account removed'); } if (this.state.connection) { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.state.connection.close(); } this.setState({connection: null, account: null}); } ); } else { this.setState({connection: null, account: null}); } } startCallFromCallKeeper(data) { utils.timestampedLog('Starting call from OS...'); let callUUID = data.callUUID || uuid.v4(); let is_conf = data.handle.search('videoconference.') === -1 ? false: true; this.backToForeground(); if (is_conf) { this.callKeepStartConference(data.handle, {audio: true, video: data.video || true, callUUID: callUUID}); } else { this.callKeepStartCall(data.handle, {audio: true, video: data.video, callUUID: callUUID}); } this._notificationCenter.removeNotification(); } selectContact(contact) { if (contact !== this.state.selectedContact) { this.setState({pinned: false}); } this.setState({selectedContact: contact}); } connectionStateChanged(oldState, newState) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Web socket', connection, 'state changed:', oldState, '->' , 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(); 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.changeRoute('/ready', 'websocket disconnected'); } break; default: if (this.state.registrationKeepalive !== true) { this.setState({loading: 'Connecting...'}); } break; } } notificationCenter() { return this._notificationCenter; } showRegisterFailure(reason) { const connection = this.getConnection(); utils.timestampedLog('Registration error: ' + reason, 'on web socket', connection); this.setState({ loading : null, registrationState: 'failed', status : { msg : 'Sign In failed: ' + reason, level : 'danger' } }); 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'); 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 === true) { 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() - }, 1500); + }, 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({loading: null, accountVerified: true, enrollment: false, autoLogin: true, registrationKeepalive: true, registrationState: 'registered' }); if (this.state.keys && !this.syncRequested) { this.syncRequested = true; console.log('Request sync messages from server', this.state.lastSyncId); this.state.account.syncConversations(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(); } } showInternalAlertPanel() { this.setState({showIncomingModal: true}); //Vibration.vibrate(VIBRATION_PATTERN, true); setTimeout(() => { Vibration.cancel(); }, 30000); } + vibrate() { + Vibration.vibrate(VIBRATION_PATTERN, true); + setTimeout(() => { + Vibration.cancel(); + }, 1000); + } + hideInternalAlertPanel() { Vibration.cancel(); this.setState({showIncomingModal: false}); } 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); /* 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(); } if (oldState === 'established' || oldState === 'accepted') { //utils.timestampedLog('Call state changed:', 'incoming call ended'); this.hideInternalAlertPanel(); } // 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') { this.startedByPush = false; //utils.timestampedLog("Incoming call was cancelled"); this.setState({showIncomingModal: false}); this.hideInternalAlertPanel(); newincomingCall = null; newCurrentCall = null; readyDelay = 10; } else if (newState === 'accepted') { //utils.timestampedLog("Incoming call was accepted"); this.hideInternalAlertPanel(); this.backToForeground(); } else if (newState === 'established') { //utils.timestampedLog("Incoming call media started"); this.hideInternalAlertPanel(); } } } 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 (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } if (!this.isConference(call)){ InCallManager.startRingback('_BUNDLE_'); } 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}); this.callKeeper.setCurrentCallActive(callUUID); 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}); } 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; 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 = 'Timeout'; 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(/5\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 { reason = 'Connection failed'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; 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 (duration > 3600) { duration = duration.format('hh:mm:ss', {trim: false}); } else { duration = duration.format('mm:ss', {trim: false}); } msg = formatted_date + " - " + direction +" " + mediaType + " call ended after " + duration; this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); } else { msg = formatted_date + " - " + direction +" " + mediaType + " call ended (" + reason + ")"; if (!server_failure) { this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); + if (reason.indexOf('PSTN calls forbidden') > -1) { + setTimeout(() => { + this.renderPurchasePSTNCredit(call.remoteIdentity.uri.toLowerCase()); + }, 2000); + } } } this.updateHistoryEntry(call.remoteIdentity.uri.toLowerCase(), callUUID, diff); this.callKeeper.endCall(callUUID, CALLKEEP_REASON); if (play_busy_tone && oldState !== 'established' && direction === 'outgoing') { this._notificationCenter.postSystemNotification('Call ended:', {body: reason}); } break; default: break; } /* utils.timestampedLog('---currentCall end:', newCurrentCall); utils.timestampedLog('---incomingCall end:', newincomingCall); */ this.setState({ currentCall: newCurrentCall, incomingCall: newincomingCall }); if (!this.state.currentCall && !this.state.incomingCall) { this.speakerphoneOn(); if (!this.state.reconnectingCall) { if (this.state.inFocus) { if (this.currentRoute !== '/ready') { utils.timestampedLog('Will go to ready in', readyDelay/1000, 'seconds (terminated)', callUUID); this.goToReadyTimer = setTimeout(() => { this.changeRoute('/ready', 'no_more_calls'); }, readyDelay); } } else { if (this.currentRoute !== '/conference') { this.changeRoute('/ready', 'no_more_calls'); } } } } if (this.state.currentCall) { //console.log('Current:', this.state.currentCall.id); } if (this.state.incomingCall) { //console.log('Incoming:', this.state.incomingCall.id); } } 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'); } } else { console.log('No call to go back to'); } } finishInviteToConference() { this.setState({inviteContacts: false, selectedContacts: []}); } goBackToHome() { this.changeRoute('/ready', 'back to home'); } goBackToHomeFromCall() { this.changeRoute('/ready', 'back to home'); if (this.state.callContact) { this.setState({selectedContact: this.state.callContact}); 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); if (this.state.account !== null && this.state.registrationState === 'registered' ) { return; } this.setState({ accountId : accountId, password : password, loading : 'Connecting...' }); 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 || '' }; 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(); storage.set('account', { accountId: this.state.accountId, password: this.state.password }); } else { this.showRegisterFailure(408); } }); } generateKeysIfNecessary(account) { console.log('Check if PGP key exists on server....') if (this.serverQueriedForPublicKey) { if (this.state.contactsLoaded) { if (!this.serverPublicKey) { console.log('We have no PGP keys here or on server'); this.generateKeys(); } else if (!this.state.keys || !this.state.keys.private){ console.log('Public PGP key exists on server but we have none'); this.setState({showImportPrivateKeyModal: true, keyDifferentOnServer: true}); } } else { console.log('Wait for PGP keys until contacts are loaded'); } } else { account.checkIfKeyExists((key) => { this.serverPublicKey = key; this.serverQueriedForPublicKey = true; if (key) { if (this.state.keys) { if (this.state.keys && this.state.keys.public !== key) { console.log('Public PGP key exists on server but is different than ours'); this.setState({showImportPrivateKeyModal: true, keyDifferentOnServer: true}) } else { console.log('Public PGP key exists on server and we have it'); } } else { if (!this.state.contactsLoaded) { console.log('Wait for PGP keys until contacts are loaded'); } else { this.showImportPrivateKeyModal(); } } } else { console.log('Public PGP key does not exist on server'); if (this.state.contactsLoaded) { this.generateKeys(); } else { console.log('Wait for PGP keys until contacts are loaded'); } } }); } } 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.setState({ loading: null }); 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}); } callKeepStartConference(targetUri, options={audio: true, video: true, participants: []}) { if (!targetUri) { return; } this.resetGoToReadyTimer(); let callUUID = options.callUUID || uuid.v4(); let participants = options.participants || null; 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}); } callKeepStartCall(targetUri, options) { this.resetGoToReadyTimer(); targetUri = targetUri.trim().toLowerCase(); if (targetUri.indexOf('@') === -1) { targetUri = targetUri + '@' + this.state.defaultDomain; } 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}); } } callKeepAcceptCall(callUUID) { // 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 utils.timestampedLog('CallKeep will answer call', callUUID); this.callKeeper.acceptCall(callUUID); this.hideInternalAlertPanel(); } callKeepRejectCall(callUUID) { // called from user interaction with Old alert panel utils.timestampedLog('CallKeep will reject call', callUUID); this.callKeeper.rejectCall(callUUID); this.hideInternalAlertPanel(); } acceptCall(callUUID) { utils.timestampedLog('User accepted call', callUUID); this.hideInternalAlertPanel(); 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; 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(); 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(); } 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); } else if (reason === 'user_hangup_conference') { utils.timestampedLog('Save conference maybe?'); setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } else if (reason === 'user_cancelled_conference') { utils.timestampedLog('Save conference maybe?'); 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}); } async showImportPrivateKeyModal() { this.setState({showImportPrivateKeyModal: true}); } async hideExportPrivateKeyModal() { this.setState({privateKey: null, showExportPrivateKeyModal: false}); } async showExportPrivateKeyModal() { this.setState({showExportPrivateKeyModal: true}); } togglePinned() { console.log('togglePinned', this.state.selectedContact); if (this.state.selectedContact) { this.getMessages(this.state.selectedContact.uri, !this.state.pinned); this.setState({pinned: !this.state.pinned}); } } toggleSpeakerPhone() { if (this.state.speakerPhoneEnabled === true) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } toggleCallMeMaybeModal() { this.setState({showCallMeMaybeModal: !this.state.showCallMeMaybeModal}); } speakerphoneOn() { utils.timestampedLog('Speakerphone On'); this.setState({speakerPhoneEnabled: true}); InCallManager.setForceSpeakerphoneOn(true); } speakerphoneOff() { utils.timestampedLog('Speakerphone Off'); this.setState({speakerPhoneEnabled: false}); InCallManager.setForceSpeakerphoneOn(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); } outgoingConference(call) { // called by sylrtc.js when an outgoing conference starts call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); this.callKeeper.startOutgoingCall(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); } 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() { 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; } this.setState({incomingCallUUID: callUUID}); this.callKeeper.handleConference(callUUID, to, from, displayName, mediaType, outgoingMedia); } startConference(targetUri, options={audio: true, video: true, participants: []}) { utils.timestampedLog('New outgoing conference to room', targetUri); this.setState({targetUri: targetUri}); this.getLocalMedia({audio: options.audio, video: options.video}, '/conference'); } escalateToConference(participants) { let outgoingMedia = {audio: true, video: true}; let mediaType = 'video'; let call; if (this.state.currentCall) { call = this.state.currentCall; } else if (this.state.incomingCall) { call = this.state.currentCall; } else { console.log('No call to escalate'); return } const localStreams = call.getLocalStreams(); if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; if (localStream.getVideoTracks().length == 0) { outgoingMedia.video = false; mediaType = 'audio'; } } this.outgoingMedia = outgoingMedia; this.participantsToInvite = participants; console.log('Escalate', mediaType, 'call', call.id, 'to conference with', participants.toString()); this.hangupCall(call.id, 'escalate_to_conference'); } conferenceInviteFromWebSocket(data) { // comes from web socket utils.timestampedLog('Conference invite from websocket', data.id, 'from', data.originator, 'for room', data.room); if (this.isConference()) { return; } //this._notificationCenter.postSystemNotification('Expecting conference invite', {body: `from ${data.originator.displayName || data.originator.uri}`}); } updateLinkingURL = (event) => { // this handles the use case where the app is running in the background and is activated by the listener... //console.log('Updated Linking url', event.url); this.eventFromUrl(event.url); DeepLinking.evaluateUrl(event.url); } eventFromUrl(url) { url = decodeURI(url); try { let direction; let event; let callUUID; let from; let to; let displayName; 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 - when Android is asleep //sylk://call/cancel//callUUID - when Android is asleep event = url_parts[2]; direction = url_parts[3]; callUUID = url_parts[4]; from = url_parts[5]; to = url_parts[6]; displayName = url_parts[7]; mediaType = url_parts[8] || 'audio'; if (event !== 'cancel' && from && from.search('@videoconference.') > -1) { event = 'conference'; to = from; } this.setState({targetUri: from}); } else if (scheme === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link direction = 'outgoing'; event = url_parts[3]; to = url_parts[4]; callUUID = uuid.v4(); if (to.indexOf('@') === -1 && event === 'conference') { to = url_parts[4] + '@' + config.defaultConferenceDomain; } else if (to.indexOf('@') === -1 && event === 'call') { to = url_parts[4] + '@' + this.state.defaultDomain; } this.setState({targetUri: to}); } 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.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, callUUID: callUUID}); } else if (direction === 'incoming') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Incoming call from', from); this.backToForeground(); 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 && this.state.blockedUris.indexOf(from) > -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; } //this.showInternalAlertPanel(); if (this.autoAcceptIncomingCall(callUUID, from)) { return; } 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; } this.callKeeper.addWebsocketCall(call); const callUUID = call.id; const from = call.remoteIdentity.uri; //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.showInternalAlertPanel(); skipNativePanel = true; } } 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() } updateServerHistory() { + this.contactsCount(); + + 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, 'with name', contact.name, 'by', origin); if (this.sql_contacts_keys.indexOf(uri) > -1) { this.updateSylkContact(uri, contact, origin); return; } let conference = contact.conference ? 1: 0; let tags = contact.tags.toString(); let media = contact.lastCallMedia.toString(); let participants = contact.participants.toString(); let unread_messages = contact.unread.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) => { - //console.log('SQL inserted contact', contact.uri, 'by', origin); + 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) => { - //console.log('SQL updated contact', contact.uri, 'by', origin); + 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) => { 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(); - const private_key = this.state.keys.private.replace(/\r/g, '').trim(); - - const publicKeyHash = await RNSimpleCrypto.SHA.sha1(public_key); - const privateKeyHash = await RNSimpleCrypto.SHA.sha1(private_key); - - const publicKeyHashContainer = "--PUBLIC KEY SHA1 CHECKSUM--" + publicKeyHash + "--"; - const privateKeyHashContainer = "--PRIVATE KEY SHA1 CHECKSUM--" + privateKeyHash + "--"; - - const keyPair = 'THIS IS THE KEY PAIR:\n' + this.state.keys.public + '\n' + this.state.keys.private + '\n' + publicKeyHashContainer + '\n' + privateKeyHashContainer; - - await OpenPGP.encryptSymmetric(keyPair, password, KeyOptions).then((encryptedBuffer) => { + 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'); } if (this.state.account) { this.state.account.syncConversations(); } } else { status = 'Private key is the same'; console.log(status); } this.setState({privateKeyImportStatus: status, privateKeyImportSuccess: true}); } else { this.setState({privateKeyImportStatus: 'Incorrect password!', privateKeyImportSuccess: false}); } } 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'); } _sendMessage(uri, text, id, contentType, timestamp) { // Send outgoing messages if (this.state.account) { //console.log('Send', contentType, 'message', id, 'to', uri); let message = this.state.account.sendMessage(uri, text, contentType, {id: id, timestamp: timestamp}, (error) => { if (error) { console.log('Message', id, 'sending error:', error); this.outgoingMessageStateChanged(id, 'failed'); let status = error.toString(); if (status.indexOf('DNS lookup error') > -1) { status = 'Domain not found'; } this.renderSystemMessage(uri, status, 'incoming'); } }); //console.log(message); //message.on('stateChanged', (oldState, newState) => {this.outgoingMessageStateChanged(message.id, oldState, newState)}) } } textToGiftedMessage(text) { return { _id: uuid.v4(), text: text, createdAt: new Date(), received: false, direction: 'outgoing', user: {} }; } async sendMessage(uri, message) { message.sent = false; message.received = false; message.pending = true; message.direction = 'outgoing'; let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) === -1) { renderMessages[uri] = []; } let public_keys; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys) { public_keys = this.state.keys.public + "\n" + this.state.myContacts[uri].publicKey; } if (!message.contentType) { message.contentType = 'text/plain'; } if (message.contentType !== 'text/pgp-public-key' && public_keys && this.state.keys) { await OpenPGP.encrypt(message.text, public_keys).then((encryptedMessage) => { this._sendMessage(uri, encryptedMessage, message._id, message.contentType, message.createdAt); this.saveOutgoingMessage(uri, message, 1); }).catch((error) => { this.saveOutgoingMessage(uri, message, 2); console.log('Failed to encrypt message:', error); this.outgoingMessageStateChanged(message._id, 'failed'); }); } else { console.log('Outgoing non-encrypted message to', uri); this.saveOutgoingMessage(uri, message); this._sendMessage(uri, message.text, message._id, message.contentType, message.createdAt); } renderMessages[uri].push(message); if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = message.text.substring(0, 100); selectedContact.timestamp = message.createdAt; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact, messages: renderMessages}); } else { this.setState({messages: renderMessages}); } } async reSendMessage(message, uri) { await this.deleteMessage(message._id, uri).then((result) => { message._id = uuid.v4(); this.sendMessage(uri, message); }).catch((error) => { console.log('Failed to delete old messages'); }); } async saveOutgoingMessage(uri, message, encrypted=0) { this.saveOutgoingChatUri(uri, message.text); //console.log('saveOutgoingMessage', message.text); let unix_timestamp = Math.floor(message.createdAt / 1000); let params = [this.state.accountId, message._id, JSON.stringify(message.createdAt), unix_timestamp, message.text, "text/plain", this.state.accountId, uri, "outgoing", "1", encrypted]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, encrypted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL insert message OK'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async saveConferenceMessage(uri, message) { if (uri.indexOf('@') === -1) { uri = uri + '@videoconference.' + this.state.defaultDomain; } - console.log('saveConferenceMessage', uri); - let unix_timestamp = Math.floor(message.createdAt / 1000); let params = [this.state.accountId, message._id, JSON.stringify(message.createdAt), unix_timestamp, message.text, "text/plain", this.state.accountId, uri, "outgoing", (message.pending ? 1: 0), message.sent ? 1: 0, message.received ? 1: 0]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { console.log('SQL insert message OK'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async outgoingMessageStateChanged(id, state) { let query; // mark message status // state can be failed or accepted utils.timestampedLog('Outgoing message', id, 'is', state); - if (state === 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state === 'failed') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); if (query) { await this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } } async 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); return; } } utils.timestampedLog('Message', id, 'is', state); let query; 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 (state == 'received') { query = "UPDATE messages set sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed) { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'error') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'forbidden') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); await this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, state, reason, code); - //this.saveLastSyncId(id); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } messageStateChangedSync(obj) { // valid API states: pending -> accepted -> delivered -> displayed, // error, failed or forbidden // valid UI render states: pending, read, received let id = obj.messageId; let state = obj.state; //console.log('Sync message', id, 'state', state); let query; 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 (state == 'received') { query = "UPDATE messages set sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'failed') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'error') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (state == 'forbidden') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); this.ExecuteQuery(query).then((results) => { //console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteMessage(id, uri, local=true) { utils.timestampedLog('Message', id, 'is deleted'); let query; // TODO send request to server query = "DELETE from messages where msg_id = '" + id + "'"; //console.log(query); if (local) { this.addJournal(id, 'removeMessage', {uri: uri}); } await this.ExecuteQuery(query).then((results) => { this.deleteRenderMessage(id, uri); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteMessageSync(id, uri) { //console.log('Sync message', id, 'is deleted'); let query; query = "DELETE from messages where msg_id = '" + id + "'"; this.ExecuteQuery(query).then((results) => { this.deleteRenderMessageSync(id, uri); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async expireMessage(id, duration=300) { utils.timestampedLog('Expire message', id, 'in', duration, 'seconds after read'); // TODO expire message } async deleteRenderMessage(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let myContacts = this.state.myContacts; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; if (uri in myContacts) { myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; if (existingMessages.length > 0 && existingMessages[0].id === id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } this.setState({messages: renderedMessages, myContacts: myContacts}); } } async deleteRenderMessageSync(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; this.setState({messages: renderedMessages}); } let idx = 'remove' + id; this.remove_sync_pending_item(idx); } async sendPendingMessage(uri, text, id, contentType, timestamp) { utils.timestampedLog('Outgoing pending message', id); if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys.public) { let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; await OpenPGP.encrypt(text, public_keys).then((encryptedMessage) => { //console.log('Outgoing encrypted message to', uri); this._sendMessage(uri, encryptedMessage, id, contentType, timestamp); }).catch((error) => { console.log('Failed to encrypt message:', error); this.outgoingMessageStateChanged(id, 'failed'); //this.saveSystemMessage(uri, 'Failed to encrypt message', 'outgoing'); }); } else { //console.log('Outgoing non-encrypted message to', uri); this._sendMessage(uri, text, id, contentType, timestamp); } } async sendPendingMessages() { if (this.mustLogout) { return; } let content; await this.ExecuteQuery("SELECT * from messages where pending = 1 and content_type like 'text/%' and from_uri = ?", [this.state.accountId]).then((results) => { let rows = results.rows; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); content = item.content; let timestamp = new Date(item.unix_timestamp * 1000); this.sendPendingMessage(item.to_uri, content, item.msg_id, item.content_type, timestamp); } }).catch((error) => { console.log('SQL error:', error); }); await this.ExecuteQuery("SELECT * FROM messages where direction = 'incoming' and system is null and received = 0 and from_uri = ?", [this.state.accountId]).then((results) => { //console.log('SQL get messages OK'); let rows = results.rows; let imdn_msg; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); let timestamp = JSON.parse(item.timestamp, _parseSQLDate); imdn_msg = {id: item.msg_id, timestamp: timestamp, from_uri: item.from_uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { query = "UPDATE messages set received = 1 where msg_id = " + item.msg_id; //console.log(query); this.ExecuteQuery(query).then((results) => { }).catch((error) => { console.log('SQL error:', error); }); } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async updateRenderMessage(id, state, reason=null, code=null) { let query; let uri; let changes = false; //console.log('updateRenderMessage', id, state); query = "SELECT * from messages where msg_id = '" + id + "';"; //console.log(query); await this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); //console.log(item); uri = item.direction === 'outgoing' ? item.to_uri : item.from_uri; console.log('Message uri', uri, 'new state', state); if (uri in this.state.messages) { let renderedMessages = this.state.messages; renderedMessages[uri].forEach((m) => { if (m._id === id) { if (state === 'accepted') { m.pending = false; changes = true; } if (state === 'delivered') { m.sent = true; m.pending = false; changes = true; } if (state === 'displayed') { m.received = true; m.sent = true; m.pending = false; changes = true; } if (state === 'failed') { m.received = false; m.sent = false; m.pending = false; m.failed = true; changes = true; } if (state === 'pinned') { m.pinned = true; changes = true; } if (state === 'unpinned') { m.pinned = false; changes = true; } } }); if (changes) { this.setState({messages: renderedMessages}); if (state === 'failed') { reason = 'Message delivery failed: ' + reason; if (code) { reason = reason + '('+ code + ')'; } this.renderSystemMessage(uri, reason, 'incoming'); } } } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async saveOutgoingChatUri(uri, content='') { console.log('saveOutgoingChatUri', uri); let query; let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } this.lookupPublicKey(myContacts[uri]); myContacts[uri].unread = []; if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); } if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } myContacts[uri].lastMessageId = null; myContacts[uri].lastCallDuration = null; myContacts[uri].timestamp = new Date(); myContacts[uri].direction = 'outgoing'; this.setState({myContacts: myContacts}); this.saveSylkContact(uri, myContacts[uri], 'saveOutgoingChatUri'); } pinMessage(id) { let query; query = "UPDATE messages set pinned = 1 where msg_id ='" + id + "'"; //console.log(query); this.ExecuteQuery(query).then((results) => { console.log('Message', id, 'pinned'); this.updateRenderMessage(id, 'pinned') this.addJournal(id, 'pinMessage'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } unpinMessage(id) { let query; query = "UPDATE messages set pinned = 0 where msg_id ='" + id + "'"; //console.log(query); this.ExecuteQuery(query).then((results) => { this.updateRenderMessage(id, 'unpinned') this.addJournal(id, 'unPinMessage'); console.log('Message', id, 'unpinned'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async addJournal(id, action, data={}) { //console.log('Add journal entry:', action, id); this.mySyncJournal[uuid.v4()] = {id: id, action: action, data: data}; this.replayJournal(); } async replayJournal() { if (!this.state.account) { utils.timestampedLog('Sync journal later when going online...'); return; } if (this.mustLogout) { return; } let op; let executed_ops = []; Object.keys(this.mySyncJournal).forEach((key) => { if (this.mustLogout) { return; } executed_ops.push(key); op = this.mySyncJournal[key]; utils.timestampedLog('Sync journal', op.action, op.id); if (op.action === 'removeConversation') { this.state.account.removeConversation(op.id, (error) => { // TODO: add period and delete remote flags if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation was completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'readConversation') { this.state.account.markConversationRead(op.id, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'removeMessage') { this.state.account.removeMessage({id: op.id, receiver: op.data.uri}, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } }); executed_ops.forEach((key) => { delete this.mySyncJournal[key]; }); storage.set('mySyncJournal', this.mySyncJournal); this.sendPendingMessages(); } async confirmRead(uri){ if (uri.indexOf('@') === -1) { return; } if (uri 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 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; this.state.account.sendDispositionNotification(message.from_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); } sql2GiftedChat(item, content) { let image; let timestamp = new Date(item.unix_timestamp * 1000); let failed = (item.pending === 0 && item.received === 0 && item.sent === 1) ? true: false; let msg; msg = { _id: item.msg_id, text: content, image: image, encrypted: item.encrypted, createdAt: timestamp, sent: ((item.sent === 1 || item.received === 1) && !failed) ? true : false, direction: item.direction, received: item.received === 1 ? true : false, pending: (item.pending === 1 && item.sent !== 1) ? true : false, system: item.system === 1 ? true : false, failed: failed, pinned: (item.pinned === 1) ? true: false, user: item.direction == 'incoming' ? {_id: item.from_uri, name: item.from_name} : {} } return msg; } async decryptMessage(message, updateContact=false) { // encrypted // 0 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 (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; myContacts[uri].timestamp = message.timestamp; this.saveSylkContact(uri, myContacts[uri], 'decryptMessage'); this.setState({myContacts: myContacts}); } } if (uri in messages) { let render_messages = messages[uri]; if (message.content_type === 'text/html') { content = utils.html2text(content); } else if (message.content_type === 'text/plain') { content = content; } else if (message.content_type.indexOf('image/') > -1) { image = `data:${message.content_type};base64,${btoa(content)}` } msg = this.sql2GiftedChat(message, content); render_messages.push(msg); messages[uri] = render_messages; if (pending_messages.length === 0) { this.confirmRead(uri); this.setState({message: messages}); } } let params = [content, id]; this.ExecuteQuery("update messages set encrypted = 2, content = ? where msg_id = ?", params).then((result) => { //console.log('SQL updated message decrypted', id); }).catch((error) => { console.log('SQL message update error:', error); }); }).catch((error) => { let params = [id]; //console.log('Failed to decrypt message:', error); this.ExecuteQuery("update messages set encrypted = 3 where msg_id = ?", params).then((result) => { //console.log('SQL updated message decrypted', id, 'rows affected', result.rowsAffected); }).catch((error) => { console.log('SQL message update error:', error); }); }); } lookupPublicKey(contact) { if (!contact.publicKey && !contact.conference && this.state.connection) { this.state.connection.lookupPublicKey(contact.uri); } } + async contactsCount() { + let query = "SELECT count(*) as rows FROM contacts where account = ?"; + let rows; + let total; + + await this.ExecuteQuery(query, [this.state.accountId]).then((results) => { + rows = results.rows; + total = rows.item(0).rows; + console.log(total, 'total contacts'); + }).catch((error) => { + console.log('SQL error:', error); + }); + } + + async getMessages(uri, pinned=false) { if (this.state.syncConversations) { return; } + let messages = this.state.messages; + let myContacts = this.state.myContacts; let msg; let query; let rows = 0; let total = 0; + let last_messages = []; + let orig_uri; if (!uri) { query = "SELECT count(*) as rows FROM messages where (from_uri = ? and direction = 'outgoing') or (to_uri = ? and direction = 'incoming')"; await this.ExecuteQuery(query, [this.state.accountId, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'total messages'); }).catch((error) => { console.log('SQL error:', error); }); return; } - this.resetUnreadCount(uri); - - let messages_uri = uri; - let isPhoneNumber = uri ? uri.match(/^(\+|0)(\d+)$/) : false; - - if (isPhoneNumber) { - messages_uri = messages_uri + '@' + this.state.defaultDomain; - } - - console.log('Get messages with', messages_uri, 'with zoom factor', this.state.messageZoomFactor); - let messages = this.state.messages; - let myContacts = this.state.myContacts; - let last_messages = []; + orig_uri = uri; if (Object.keys(myContacts).indexOf(uri) === -1) { this.setState({messages: {}}); return; } + if (utils.isPhoneNumber(uri) && uri.indexOf('@') === -1) { + uri = uri + '@' + this.state.defaultDomain; + } else { + this.resetUnreadCount(orig_uri); + this.lookupPublicKey(myContacts[orig_uri]); + } + + console.log('Get messages with', uri, 'with zoom factor', this.state.messageZoomFactor); + let limit = this.state.messageLimit * this.state.messageZoomFactor; query = "SELECT count(*) as rows FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?))"; if (pinned) { query = query + ' and pinned = 1'; } - await this.ExecuteQuery(query, [this.state.accountId, messages_uri, messages_uri, this.state.accountId]).then((results) => { + await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'messages with', uri, 'from database'); }).catch((error) => { console.log('SQL error:', error); }); - myContacts[uri].totalMessages = total; - - this.lookupPublicKey(myContacts[uri]); + myContacts[orig_uri].totalMessages = total; query = "SELECT * FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?)) "; if (pinned) { query = query + ' and pinned = 1'; } query = query + ' order by unix_timestamp desc limit ?, ?'; - await this.ExecuteQuery(query, [this.state.accountId, messages_uri, messages_uri, this.state.accountId, this.state.messageStart, limit]).then((results) => { - //console.log('SQL get messages OK', results.rows.length); + await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId, this.state.messageStart, limit]).then((results) => { + console.log('SQL get messages OK', results.rows.length); let rows = results.rows; - messages[uri] = []; + 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; for (let i = 0; i < rows.length; i++) { var item = rows.item(i); if (!item.content || !item.msg_id) { //console.log('Remove broken message', item); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); - myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; + myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } content = item.content; if (!content) { content = 'Broken 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[uri].totalMessages = myContacts[uri].totalMessages - 1; + myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; if (item.encrypted === null) { item.encrypted = 1; } enc = parseInt(item.encrypted); if (enc && enc !== 3 ) { if (uri in decryptingMessages) { } else { - decryptingMessages[uri] = []; + decryptingMessages[orig_uri] = []; } - decryptingMessages[uri].push(item.msg_id); + decryptingMessages[orig_uri].push(item.msg_id); messages_to_decrypt.push(item); } } else { if (item.content_type === 'text/html') { content = utils.html2text(content); } else if (item.content_type === 'text/plain') { content = content; } else if (item.content_type.indexOf('image/') > -1) { image = `data:${item.content_type};base64,${btoa(content)}` } else if (item.content_type === 'application/sylk-contact-update') { - myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; + myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; console.log('Remove update contact message', item.id); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); continue; } else { console.log('Unknown message', item.msg_id, 'type', item.content_type); - myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; + myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } msg = this.sql2GiftedChat(item, content); - messages[uri].push(msg); + messages[orig_uri].push(msg); } } - console.log('Got', messages[uri].length, 'out of', total, 'messages for', uri); + console.log('Got', messages[orig_uri].length, 'out of', total, 'messages for', uri); - last_messages = messages[uri]; + last_messages = messages[orig_uri]; last_messages.reverse(); if (last_messages.length > 0) { last_messages.forEach((last_item) => { if (!last_item.image && !last_item.system) { last_message = last_item.text.substring(0, 100); last_message_id = last_item.id; } else { return; } }); } - if (uri in myContacts) { - if (last_message && last_message != myContacts[uri].lastMessage) { - myContacts[uri].lastMessage = last_message; - myContacts[uri].lastMessageId = last_message_id; - this.saveSylkContact(uri, myContacts[uri], 'getMessages'); + if (orig_uri in myContacts) { + if (last_message && last_message != myContacts[orig_uri].lastMessage) { + myContacts[orig_uri].lastMessage = last_message; + myContacts[orig_uri].lastMessageId = last_message_id; + this.saveSylkContact(uri, myContacts[orig_uri], 'getMessages'); this.setState({myContacts: myContacts}); } } this.setState({messages: messages, decryptingMessages: decryptingMessages}); let i = 1; messages_to_decrypt.forEach((item) => { var updateContact = messages_to_decrypt.length === i; //console.log('To decrypt', messages_to_decrypt.length, 'updateContact =', updateContact); this.decryptMessage(item, updateContact); i = i + 1; }); }).catch((error) => { console.log('SQL error:', error); }); } async deleteMessages(uri, local=true) { console.log('Delete messages for', uri); let myContacts = this.state.myContacts; let query; let params; + let wipe = false; + let orig_uri = uri; + if (uri) { - 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]; - if (local) { - this.addJournal(uri, 'removeConversation'); - let conf_uri = uri; - if (uri.indexOf('@') === -1) { - const conf_uri = uri + '@videoconference.' + this.state.defaultDomain; + if (uri.indexOf('@') === -1 && utils.isPhoneNumber(uri)) { + uri = uri + '@' + this.state.defaultDomain; + } else { + if (local) { + this.addJournal(orig_uri, 'removeConversation'); } } + } + + if (uri) { + 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 { + wipe = true; + console.log('--- Wiping device --- '); query = "DELETE FROM messages where (to_uri = ? and direction = 'incoming') or (from_uri = ? and direction = 'outgoing')"; params = [this.state.accountId, this.state.accountId]; uri = this.state.accountId; this.setState({messages: {}, myContacts: {}}); this.saveLastSyncId(null); } await this.ExecuteQuery(query, params).then((result) => { console.log('SQL deleted', result.rowsAffected, 'messages'); if (result.rowsAffected) { this._notificationCenter.postSystemNotification(result.rowsAffected + ' messages removed'); } - if (uri === this.state.accountId) { + + if (wipe) { this.ExecuteQuery('delete from contacts where account = ?', [this.state.accountId]); setTimeout(() => { this.logout(); }, 3000); } else { - this.removeContact(uri); + this.removeContact(orig_uri); } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } playIncomingSound() { let must_play_sound = true; if (this.msg_sound_played_ts) { let diff = (Date.now() - this.msg_sound_played_ts)/ 1000; if (diff < 10) { must_play_sound = false; } } this.msg_sound_played_ts = Date.now(); if (must_play_sound) { try { if (Platform.OS === 'ios') { SoundPlayer.setSpeaker(true); } SoundPlayer.playSoundFile('message_received', 'wav'); } catch (e) { console.log('Cannot play message_received.wav', 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); + console.log('SQL deleted', result.rowsAffected, 'messages with', uri, 'before', obj.timestamp); } }).catch((error) => { console.log('SQL delete conversation sync error:', error); }); let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].timestamp < obj.timestamp) { this.deleteSylkContact(uri); } } async readConversation(obj) { let uri = obj; this.resetUnreadCount(uri) } removeContact(uri) { let myContacts = this.state.myContacts; this.deleteSylkContact(uri); + if (this.state.selectedContact && this.state.selectedContact.uri === uri) { + this.setState({selectedContact: null}); + } + let renderMessages = this.state.messages; if (uri in renderMessages) { delete renderMessages[uri]; this.setState({messages: renderMessages}); } } add_sync_pending_item(item) { if (this.sync_pending_items.indexOf(item) > -1) { return; } this.sync_pending_items.push(item); if (this.sync_pending_items.length == 1) { //console.log('Sync started ---'); this.setState({syncConversations: true}); if (this.syncTimer === null) { this.syncTimer = setTimeout(() => { this.resetSyncTimer(); }, 1000 * 60); } } } resetSyncTimer() { if (this.sync_pending_items.length > 0) { this.sync_pending_items = []; console.log('Sync ended by timer ---'); //console.log('Pending tasks:', this.sync_pending_items); this.afterSyncTasks(); } } remove_sync_pending_item(item) { //console.log('remove_sync_pending_item', this.sync_pending_items.length); let idx = this.sync_pending_items.indexOf(item); if (idx > -1) { this.sync_pending_items.splice(idx, 1); } if (this.sync_pending_items.length == 0 && this.state.syncConversations) { if (this.syncTimer !== null) { clearTimeout(this.syncTimer); this.syncTimer = null; } this.afterSyncTasks(); } else { if (this.sync_pending_items.length > 10 && this.sync_pending_items.length % 10 == 0) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } else if (this.sync_pending_items.length > 0 && this.sync_pending_items.length < 10) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } } } async insertPendingMessages() { let query = "INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES " if (this.pendingNewSQLMessages.length > 0) { //console.log('Inserting', this.pendingNewSQLMessages.length, 'new messages'); } let pendingNewSQLMessages = this.pendingNewSQLMessages; this.pendingNewSQLMessages = []; let all_values = []; let n = 0; let i = 1; if (pendingNewSQLMessages.length > 0) { pendingNewSQLMessages.forEach((values) => { Array.prototype.push.apply(all_values, values); query = query + "("; n = 0; while (n < values.length ) { query = query + "?" if (n < values.length - 1) { query = query + ","; } n = n + 1; } query = query + ")"; if (pendingNewSQLMessages.length > i) { query = query + ", "; } i = i + 1; }); this.ExecuteQuery(query, all_values).then((result) => { //console.log('SQL inserted', pendingNewSQLMessages.length, 'messages'); this.newSyncMessagesCount = this.newSyncMessagesCount + pendingNewSQLMessages.length; - }).catch((error) => { - console.log('SQL error inserting bulk messages:', error); - + console.log('SQL error inserting bulk messages:', error.message); console.log('query:', query); + pendingNewSQLMessages.forEach((values) => { + this.ExecuteQuery(query, values).then((result) => { + this.newSyncMessagesCount = this.newSyncMessagesCount + 1; + }).catch((error) => { + if (error.message.indexOf('SQLITE_CONSTRAINT_PRIMARYKEY') > -1) { + console.log('Duplicate message id', values[2]); + } else { + console.log('SQL error inserting message', values[2], error.message); + } + }); + }); }); } else { console.log('No messages to insert'); } } async afterSyncTasks() { this.insertPendingMessages(); if (this.newSyncMessagesCount) { console.log('Synced', this.newSyncMessagesCount, 'messages from server'); this.newSyncMessagesCount = 0; } this.setState({syncConversations: false}); this.sync_pending_items = []; let myContacts = this.state.myContacts; let updateContactUris = this.state.updateContactUris; let replicateContacts = this.state.replicateContacts; let deletedContacts = this.state.deletedContacts; //console.log('updateContactUris:', Object.keys(updateContactUris).toString()); //console.log('replicateContacts:', Object.keys(replicateContacts).toString()); //console.log('deletedContacts:', Object.keys(deletedContacts).toString()); let uris = Object.keys(replicateContacts).concat(Object.keys(updateContactUris)); uris = [... new Set(uris)]; //console.log('Update contacts:', uris.toString()); // sync changed myContacts with SQL database let created; let old_tags; uris.forEach((uri) => { if (uri in myContacts) { created = false; } else { if (uri in deletedContacts) { return } myContacts[uri] = this.newContact(uri); created = true; } if (uri in replicateContacts) { myContacts[uri].name = replicateContacts[uri].name; + myContacts[uri].email = replicateContacts[uri].email; myContacts[uri].organization = replicateContacts[uri].organization; + old_tags = myContacts[uri].tags; myContacts[uri].tags = replicateContacts[uri].tags; myContacts[uri].participants = replicateContacts[uri].participants; if (myContacts[uri].timestamp > replicateContacts[uri].timestamp) { if (old_tags.indexOf('missed') > -1 && replicateContacts[uri].tags.indexOf('missed') === -1) { myContacts[uri].tags.push('missed'); } } if (old_tags.indexOf('chat') > -1 && replicateContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (old_tags.indexOf('history') > -1 && replicateContacts[uri].tags.indexOf('history') === -1) { myContacts[uri].tags.push('history'); } if (replicateContacts[uri].timestamp > myContacts[uri].timestamp || created) { myContacts[uri].timestamp = replicateContacts[uri].timestamp; if (uri === this.state.accountId) { let name = replicateContacts[uri].name || ''; let organization = replicateContacts[uri].organization || ''; - this.setState({displayName: name, organization: organization}); + 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'); if (diff > 3) { - console.log('Sync ended after', diff, 'seconds'); this._notificationCenter.postSystemNotification('Messages in sync with server'); } + } setTimeout(() => { this.addTestContacts(); this.refreshNavigationItems(); }, 2000); } async syncConversations(messages) { if (this.sync_pending_items.length > 0) { console.log('Sync already in progress'); return; } if (this.mustLogout || this.currentRoute === '/logout') { return; } if (this.currentRoute === '/login') { return; } this.syncStartTimestamp = new Date(); let myContacts = this.state.myContacts; let renderMessages = this.state.messages; if (messages.length > 0) { console.log('Sync', messages.length, 'events from server'); this._notificationCenter.postSystemNotification('Syncing messages with the server'); this.add_sync_pending_item('sync_in_progress'); + } else { + this.setState({firstSyncDone: true}); + console.log('Sync ended'); + setTimeout(() => { + this.updateServerHistory() + }, 1000); } let i = 0; let idx; let uri; let last_id; let content; let contact; let existingMessages; let formatted_date; let newMessages = []; let lastMessages = {}; let updateContactUris = {}; let deletedContacts = {}; let last_timestamp; let stats = {state: 0, remove: 0, incoming: 0, outgoing: 0, delete: 0, read: 0} let purgeMessages = this.state.purgeMessages; - let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; messages.forEach((message) => { if (this.mustLogout) { return; } last_timestamp = message.timestamp; i = i + 1; uri = null; if (message.contentType === 'application/sylk-message-remove') { uri = message.content.contact; } else if (message.contentType === 'application/sylk-conversation-remove') { uri = message.content; } else if (message.contentType === 'application/sylk-conversation-read' ) { uri = message.content; } else if (message.contentType === 'message/imdn') { } else { if (message.sender.uri === this.state.account.id) { uri = message.receiver; } else { uri = message.sender.uri; } } if (uri) { //console.log('Process journal', i, 'of', messages.length, message.contentType, uri, message.timestamp); } let d = new Date(2019); if (message.timestamp < d) { console.log('Skip broken journal with broken date', message.id); purgeMessages.push(message.id); return; } if (!message.content) { console.log('Skip broken journal with empty body', message.id); purgeMessages.push(message.id); return; } if (message.contentType !== 'application/sylk-conversation-remove' && message.contentType !== 'application/sylk-message-remove' && uri && Object.keys(myContacts).indexOf(uri) === -1) { - - if (uri.indexOf('@') > -1 && !email_reg.test(uri)) { + if (uri.indexOf('@') > -1 && !this.isEmailAddress(uri)) { return; } console.log('Will add a new contact', uri); myContacts[uri] = this.newContact(uri); myContacts[uri].timestamp = message.timestamp; //this.setState({myContacts: myContacts}); } //console.log('Sync', message.timestamp, message.contentType, uri); if (message.contentType === 'application/sylk-message-remove') { idx = 'remove' + message.id; this.add_sync_pending_item(idx); - this.deleteMessageSync(message.id, uri); if (uri in renderMessages) { existingMessages = renderMessages[uri]; newMessages = []; existingMessages.forEach((msg) => { if (msg.id === message.id) { return; } newMessages.push(msg); }); renderMessages[uri] = newMessages; } if (uri in myContacts) { let idx = myContacts[uri].unread.indexOf(message.id); if (idx > -1) { myContacts[uri].unread.splice(idx, 1); } if (myContacts[uri].lastMessageId === message.id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } if (uri in lastMessages && lastMessages[uri] === message.id) { delete lastMessages[uri]; } stats.delete = stats.delete + 1; } else if (message.contentType === 'application/sylk-conversation-remove') { if (uri in myContacts && message.timestamp > myContacts[uri].timestamp) { delete myContacts[uri]; } if (uri in updateContactUris) { delete updateContactUris[uri]; } if (uri in lastMessages) { delete lastMessages[uri]; } if (uri in renderMessages) { delete renderMessages[uri]; } deletedContacts[uri] = message; stats.remove = stats.remove + 1; } else if (message.contentType === 'application/sylk-conversation-read') { updateContactUris[uri] = last_timestamp; myContacts[uri].unread = []; stats.read = stats.read + 1; } else if (message.contentType === 'message/imdn') { this.messageStateChangedSync({messageId: message.id, state: message.state}); stats.state = stats.state + 1; } else { this.add_sync_pending_item(message.id); if (message.sender.uri === this.state.account.id) { if (message.contentType !== 'application/sylk-contact-update') { if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } myContacts[uri].lastMessageId = message.id; myContacts[uri].lastMessage = null; // need to be loaded later after decryption myContacts[uri].lastCallDuration = null; myContacts[uri].direction = 'outgoing'; if (myContacts[uri].tags.indexOf('chat') === -1 && (message.contentType === 'text/plain' || message.contentType === 'text/html')) { myContacts[uri].tags.push('chat'); } lastMessages[uri] = message.id; if (message.timestamp > myContacts[uri].timestamp) { updateContactUris[uri] = message.timestamp; myContacts[uri].timestamp = message.timestamp; } } stats.outgoing = stats.outgoing + 1; this.outgoingMessageSync(message); } else { if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } if (message.timestamp > myContacts[uri].timestamp) { updateContactUris[uri] = message.timestamp; myContacts[uri].timestamp = message.timestamp; } myContacts[uri].lastMessageId = message.id; myContacts[uri].lastMessage = null; // need to be loaded later after decryption myContacts[uri].lastCallDuration = null; myContacts[uri].direction = 'incoming'; if (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, - firstSyncDone: true + purgeMessages: purgeMessages }); this.remove_sync_pending_item('sync_in_progress'); Object.keys(lastMessages).forEach((uri) => { //console.log('Update last message for', uri); // TODO update lastMessage content for each contact }); if (last_id) { this.saveLastSyncId(last_id, true); } } async publicKeyReceived(message) { if (message.publicKey) { this.savePublicKey(message.uri, message.publicKey.trim()); } else { console.log('No public key available on server for', message.uri); if (message.uri === this.state.accountId) { var uri = uuid.v4() + '@' + this.state.defaultDomain; //console.log('Send 1st public to', uri); this.sendPublicKey(uri); } } } async incomingMessage(message) { console.log('Message', message.id, message.contentType, 'was received'); // Handle incoming messages + this.saveLastSyncId(message.id); + if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'application/sylk-contact-update') { return; } if (message.contentType === 'text/pgp-public-key') { this.savePublicKey(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-private-key' && message.sender.uri === this.state.account.id) { console.log('Received PGP private key from another device'); this.processRemotePrivateKey(message.content); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; if (is_encrypted) { if (!this.state.keys || !this.state.keys.private) { console.log('Missing private key, cannot decrypt message'); this.saveSystemMessage(message.sender.uri, 'Cannot decrypt: 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:', error); this.sendPublicKey(message.sender.uri); //this.saveSystemMessage(message.sender.uri, 'Cannot decrypt: wrong public key', 'incoming'); }); } } else { //console.log('Incoming message is not encrypted'); this.handleIncomingMessage(message); } - - //this.saveLastSyncId(message.id); } handleIncomingMessage(message, decryptedBody=null) { let content = decryptedBody || message.content; //this.sendLocalAndroidNotification(message.sender.uri, content); this.saveIncomingMessage(message, decryptedBody); let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(message.sender.uri) === -1) { renderMessages[message.sender.uri] = []; } renderMessages[message.sender.uri].push(utils.sylkToRenderMessage(message, decryptedBody, 'incoming')); if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = content.substring(0, 100); selectedContact.timestamp = message.timestamp; selectedContact.direction = 'incoming'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact, messages: renderMessages}); } else { this.setState({messages: renderMessages}); } + + this.notifyIncomingMessageWhileInACall(message.sender.uri); } async incomingMessageSync(message) { //console.log('Sync incoming message', message); // Handle incoming messages if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); this.savePublicKeySync(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-public-key-imported') { return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { this.saveIncomingMessageSync(message, null, true); } else { //console.log('Incoming message', message.id, 'not encrypted from', message.sender.uri); this.saveIncomingMessageSync(message); } this.remove_sync_pending_item(message.id); } async outgoingMessage(message) { console.log('Outgoing message', message.id, 'to', message.receiver); + this.saveLastSyncId(message.id); + if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'text/pgp-public-key') { return; } if (message.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); - //this.saveLastSyncId(message.id); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = myContacts[uri].lastMessage; selectedContact.timestamp = message.timestamp; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact}); } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > -1) { renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); this.setState({renderMessages: renderMessages}); } } 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); - //this.saveLastSyncId(message.id); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > -1) { renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); this.setState({renderMessages: renderMessages}); } this.saveSylkContact(uri, myContacts[uri], 'outgoingMessage'); } } } async outgoingMessageSync(message) { //console.log('Sync outgoing message', message.id, 'to', message.receiver); if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'message/imdn') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { if (message.contentType === 'application/sylk-contact-update') { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Sync outgoing message', message.id, message.contentType, 'decrypted to', message.receiver); this.handleReplicateContactSync(decryptedBody, message.id, message.timestamp); this.remove_sync_pending_item(message.id); }).catch((error) => { console.log('Failed to decrypt my own message in sync:', error); this.remove_sync_pending_item(message.id); return; }); } else { this.saveOutgoingMessageSqlBatch(message, null, true); this.remove_sync_pending_item(message.id); } } else { if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContactSync(content, message.id, message.timestamp); this.remove_sync_pending_item(message.id); } else { this.saveOutgoingMessageSqlBatch(message); } } } saveOutgoingMessageSql(message, decryptedBody=null, is_encrypted=false) { let pending = 0; let sent = 0; let received = null; let failed = 0; let encrypted = 0; let content = decryptedBody || message.content; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } if (message.state == 'pending') { pending = 1; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (message.state == 'failed') { sent = 1; received = 0; failed = 1; } else if (message.state == 'error') { sent = 1; received = 0; failed = 1; } else if (message.state == 'forbidden') { sent = 1; received = 0; } let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL inserted outgoing', message.contentType, 'message to', message.receiver, 'encrypted =', encrypted); this.remove_sync_pending_item(message.id); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } this.remove_sync_pending_item(message.id); }); } async saveOutgoingMessageSqlBatch(message, decryptedBody=null, is_encrypted=false) { let pending = 0; let sent = 0; let received = null; let failed = 0; let encrypted = 0; let content = decryptedBody || message.content; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } if (message.state == 'pending') { pending = 1; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (message.state == 'failed') { sent = 1; received = 0; failed = 1; } else if (message.state == 'error') { sent = 1; received = 0; failed = 1; } else if (message.state == 'forbidden') { sent = 1; received = 0; } let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; this.pendingNewSQLMessages.push(params); if (this.pendingNewSQLMessages.length > 34) { this.insertPendingMessages(); } this.remove_sync_pending_item(message.id); } async saveSystemMessage(uri, content, direction, missed=false) { let timestamp = new Date(); let unix_timestamp = Math.floor(timestamp / 1000); let id = uuid.v4(); let params = [this.state.accountId, id, JSON.stringify(timestamp), unix_timestamp, content, 'text/plain', direction === 'incoming' ? uri : this.state.account.id, direction === 'outgoing' ? uri : this.state.account.id, 0, 1, direction]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, pending, system, direction) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { this.renderSystemMessage(uri, content, direction, timestamp); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } + async renderPurchasePSTNCredit(uri) { + let url = 'https://mdns.sipthor.net/sip_settings.phtml?account='+ this.state.accountId + '&tab=credit'; + let myContacts = this.state.myContacts; + + if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { + uri = uri.split('@')[0]; + } + + let renderMessages = this.state.messages; + if (Object.keys(renderMessages).indexOf(uri) > - 1) { + let msg; + + msg = { + _id: uuid.v4(), + text: 'To call phone numbers, you must purchase credit at ' + url, + createdAt: new Date(), + direction: 'incoming', + sent: true, + pending: false, + failed: false, + user: {_id: uri, name: uri} + } + + renderMessages[uri].push(msg); + this.setState({renderMessages: renderMessages}); + } + } + async renderSystemMessage(uri, content, direction, timestamp) { + let myContacts = this.state.myContacts; + + if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { + uri = uri.split('@')[0]; + } + let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > - 1) { let msg; msg = { _id: uuid.v4(), text: content, createdAt: timestamp, direction: direction, sent: true, pending: false, system: true, failed: false, user: direction == 'incoming' ? {_id: uri, name: uri} : {} } renderMessages[uri].push(msg); this.setState({renderMessages: renderMessages}); } } async saveIncomingMessage(message, decryptedBody=null) { let myContacts = this.state.myContacts; let uri = message.sender.uri; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } var content = decryptedBody || message.content; let received = 1; let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, this.state.account.id, "incoming", received]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { if (myContacts[uri].name === null || myContacts[uri].name === '' && message.sender.displayName) { myContacts[uri].name = message.sender.displayName; } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } myContacts[uri].unread.push(message.id); myContacts[uri].direction = 'incoming'; myContacts[uri].lastCallDuration = null; if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { content = 'Image'; } if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { myContacts[uri].lastMessage = content.substring(0, 100); myContacts[uri].lastMessageId = message.id; this.setState({myContacts: myContacts}); } this.updateTotalUread(myContacts); this.saveSylkContact(uri, myContacts[uri], 'saveIncomingMessage'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } saveIncomingMessageSync(message, decryptedBody=null, is_encrypted=false) { var content = decryptedBody || message.content; let encrypted = 0; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } let received = 0; let imdn_msg; //console.log('saveIncomingMessageSync', message); if (message.dispositionNotification.indexOf('display') === -1) { //console.log('Incoming message', message.id, 'was already read'); received = 2; } else { if (message.dispositionNotification.indexOf('positive-delivery') > -1) { imdn_msg = {id: message.id, timestamp: message.timestamp, from_uri: message.sender.uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { received = 1; } } else { received = 1; } } let pending let sent; let unix_timestamp = Math.floor(message.timestamp / 1000); let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, this.state.account.id, "incoming", pending, sent, received]; this.pendingNewSQLMessages.push(params); this.remove_sync_pending_item(message.id); if (this.pendingNewSQLMessages.length > 34) { this.insertPendingMessages() } } saveParticipant(callUUID, room, uri) { if (this._historyConferenceParticipants.has(callUUID)) { let old_participants = this._historyConferenceParticipants.get(callUUID); if (old_participants.indexOf(uri) === -1) { old_participants.push(uri); } } else { let new_participants = [uri]; this._historyConferenceParticipants.set(callUUID, new_participants); } if (!this.myParticipants) { this.myParticipants = new Object(); } if (this.myParticipants.hasOwnProperty(room)) { let old_uris = this.myParticipants[room]; if (old_uris.indexOf(uri) === -1 && uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { this.myParticipants[room].push(uri); } } else { let new_uris = []; if (uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { new_uris.push(uri); } if (new_uris) { this.myParticipants[room] = new_uris; } } storage.set('myParticipants', this.myParticipants); } deleteContact(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { this.deleteMessages(uri); } } deletePublicKey(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { myContacts[uri].publicKey = null; console.log('Public key of', uri, 'deleted'); this.saveSylkContact(uri, myContacts[uri], 'deletePublicKey'); } } newContact(uri, name=null, data={}) { //console.log('Create new contact', uri, data); let current_datetime = new Date(); 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, 'newContact'); 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) { + if (uri.indexOf('@') === -1 && !utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } - //console.log('Save contact', uri); - let myContacts = this.state.myContacts; if (uri in myContacts) { - // + contact = myContacts[uri]; } else { - myContacts[uri] = this.newContact(uri); + contact = this.newContact(uri); + if (!contact) { + return; + } } - myContacts[uri].organization = organization; - myContacts[uri].name = displayName; - myContacts[uri].uri = uri; - myContacts[uri].email = email; - myContacts[uri].timestamp = new Date(); + contact.organization = organization; + contact.name = displayName; + contact.uri = uri; + contact.email = email; + contact.timestamp = new Date(); - myContacts[uri] = this.sanitizeContact(uri, myContacts[uri]); + contact = this.sanitizeContact(uri, contact); + + if (!contact) { + this._notificationCenter.postSystemNotification('Invalid contact ' + uri); + return; + } - if (!myContacts[uri].photo) { - var name_idx = myContacts[uri].name.trim().toLowerCase(); + if (!contact.photo) { + var name_idx = contact.name.trim().toLowerCase(); if (name_idx in this.state.avatarPhotos) { - myContacts[uri].photo = this.state.avatarPhotos[name_idx]; + contact.photo = this.state.avatarPhotos[name_idx]; } } - this.replicateContact(myContacts[uri]); + this.replicateContact(contact); - this.saveSylkContact(uri, myContacts[uri], 'saveContact'); + 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 aprivate 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; contact = JSON.parse(json_contact); if (contact.uri === null) { return; } if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } let uri = contact.uri; let myContacts = this.state.myContacts; if (uri in myContacts) { + contact = myContacts[uri]; // } else { - myContacts[uri] = this.newContact(uri, contact.name); + contact = this.newContact(uri, contact.name); + if (!contact) { + return; + } } - myContacts[uri].uri = uri; - myContacts[uri].name = contact.name; - myContacts[uri].email = contact.email; - myContacts[uri].organization = contact.organization; - myContacts[uri].timestamp = new Date(contact.timestamp * 1000); - myContacts[uri].tags = contact.tags; - myContacts[uri].participants = contact.participants; + contact.uri = uri; + contact.name = contact.name; + contact.email = contact.email; + contact.organization = contact.organization; + contact.timestamp = new Date(contact.timestamp * 1000); + contact.tags = contact.tags; + contact.participants = contact.participants; - this.saveSylkContact(uri, myContacts[uri], 'handleReplicateContact'); + this.saveSylkContact(uri, contact, 'handleReplicateContact'); } async handleReplicateContactSync(json_contact, id, msg_timestamp) { let purgeMessages = this.state.purgeMessages; let contact; contact = JSON.parse(json_contact); let timestamp = msg_timestamp; let uri = contact.uri; if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } if (contact.timestamp) { timestamp = new Date(contact.timestamp * 1000); } let replicateContacts = this.state.replicateContacts; - console.log('Replicate contact', uri, 'with name', contact.name); - if (uri in replicateContacts) { if (timestamp < replicateContacts[uri].timestamp) { - purgeMessages.push(id); - this.setState({purgeMessages: purgeMessages}); - return; + //purgeMessages.push(id); + //this.setState({purgeMessages: purgeMessages}); + console.log('Sync replicate contact skipped because is too old', timestamp, uri, 'with name', contact.name); + return; } // } 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, 'with name', contact.name); + replicateContacts[uri].uri = uri; 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) { //console.log('sanitizeContact', uri, contact); let idx; if (!uri || uri === '') { return null; } - let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; uri = uri.trim().toLowerCase(); - if (uri.indexOf('@') > -1) { - let validEmail = email_reg.test(uri); - if (!validEmail) { - console.log('sanitizeContact failed:', contact); - return null; - } + if (!utils.isPhoneNumber(uri) && !utils.isEmailAddress(uri)) { + console.log('sanitizeContact 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 in myContacts) { } else { myContacts[uri] = this.newContact(uri); } let blocked; idx = myContacts[uri].tags.indexOf('blocked'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); blocked = false; } else { myContacts[uri].tags.push('blocked'); blocked = true; } myContacts[uri].timestamp = new Date(); this.saveSylkContact(uri, myContacts[uri], 'toggleBlocked'); let idx = blockedUris.indexOf(uri); if (idx === -1 && blocked) { blockedUris.push(uri); } else if (idx > -1 && !blocked) { blockedUris.splice(idx, 1); } this.replicateContact(myContacts[uri]); this.setState({blockedUris: blockedUris, selectedContact: null}); } updateBlocked(uri, blocked) { if (blocked === null) { return; } let blockedUris = this.state.blockedUris; let idx; idx = blockedUris.indexOf(uri); if (blocked && idx === -1) { blockedUris.push(uri); this.setState({blockedUris: blockedUris}); } else if (!blocked && idx > -1) { blockedUris.splice(idx, 1); this.setState({blockedUris: blockedUris}); } else { return; } } appendInvitedParties(room, uris) { //console.log('Save invited parties', uris, 'for room', room); let myInvitedParties = this.state.myInvitedParties; let current_uris = myInvitedParties.hasOwnProperty(room) ? myInvitedParties[room] : []; uris.forEach((uri) => { let idx = current_uris.indexOf(uri); if (idx === -1) { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } if (uri !== this.state.account.id) { current_uris.push(uri); //console.log('Added', uri, 'to room', room); } } }); this.saveConference(room, uris); } shareContent() { if (this.state.shareContent.length === 0) { return; } if (this.state.selectedContacts.length === 0) { this._notificationCenter.postSystemNotification('Sharing canceled'); } let item = this.state.shareContent[0]; let content = ''; if (item.subject) { content = content + '\n\n' + item.subject; } if (item.text) { content = content + '\n\n' + item.text; } if (item.weblink) { content = content + '\n\n' + item.weblink; } content = content.trim(); this.state.selectedContacts.forEach((uri) => { let msg = this.textToGiftedMessage(content); console.log('Share external item with', uri); this.sendMessage(uri, msg); }); ReceiveSharingIntent.clearReceivedFiles(); this.setState({shareContent: [], selectedContacts: [], shareToContacts: false}); } saveConference(room, participants, displayName=null) { let uri = room; console.log('Save conference', room, 'with display name', displayName, 'and participants', participants); if (room.indexOf('@') === -1) { room = room + '@videoconference.' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } myContacts[uri].timestamp = new Date(); if (displayName) { 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; } - console.log('updateHistoryEntry', uri, callUUID, duration); + //console.log('updateHistoryEntry', uri, callUUID, duration); let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].lastCallId === callUUID) { 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())}> ); } 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; history.forEach((item) => { uri = item.uri; if (this.state.blockedUris.indexOf(uri) > -1) { return; } if (uri in myContacts) { } else { - myContacts[uri] = this.newContact(uri); + contact = this.newContact(uri); + + if (!contact) { + return; + } + + myContacts[uri] = contact; myContacts[uri].timestamp = item.timestamp; myContacts[uri].name = item.name; } if (item.timestamp > myContacts[uri].timestamp) { } else { if (myContacts[uri].lastCallId === item.sessionId) { return; } } if (item.timestamp && item.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = item.timestamp; } 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); } } else { idx = tags.indexOf('missed'); if (idx > -1) { tags.splice(idx, 1); } } tags.push('history'); if (item.displayName && !myContacts[uri].name) { myContacts[uri].name = item.displayName; } 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; myContacts[uri].tags = tags; i = i + 1; this.updateTotalUread(myContacts); this.saveSylkContact(uri, this.state.myContacts[uri], 'saveHistory'); }); if (i > 0) { console.log('Saved new', i, 'history items'); } else { //console.log('Server history is already in sync'); } this.setState({missedCalls: missedCalls}); } hideLogsModal() { this.setState({showLogsModal: false}); } purgeLogs() { RNFS.unlink(logfile) .then(() => { utils.timestampedLog('Log file initialized'); this.showLogs(); }) // `unlink` will throw an error, if the item to unlink does not exist .catch((err) => { console.log(err.message); }); } showLogs() { this.setState({showLogsModal: true}); RNFS.readFile(logfile, 'utf8').then((content) => { console.log('Read', content.length, 'bytes from', logfile); const lastlines = content.split('\n').slice(-MAX_LOG_LINES).join('\n'); this.setState({logs: lastlines}); }); } trimLogs() { RNFS.readFile(logfile, 'utf8').then((content) => { const lines = content.split('\n'); //console.log('Read', lines.length, 'lines and', content.length, 'bytes from', logfile); if (lines.length > (MAX_LOG_LINES + 50) || content.length > 100000) { const text = lines.slice(-MAX_LOG_LINES).join('\n'); RNFS.writeFile(logfile, text + '\r\n', 'utf8') .then((success) => { //console.log('Trimmed logs to', MAX_LOG_LINES, 'lines and', text.length, 'bytes'); }) .catch((err) => { console.log(err.message); }); } }); } ready() { let publicKey; let call = this.state.currentCall || this.state.incomingCall; if (this.state.selectedContact) { const uri = this.state.selectedContact.uri; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey) { publicKey = this.state.myContacts[uri].publicKey; } } else { publicKey = this.state.keys ? this.state.keys.public: null; } return ( ); } preview() { return ( ); } 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 ( ) } 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; } 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.serverPublicKey = null; this.serverQueriedForPublicKey = false; this.callKeeper.setAvailable(false); + this.sql_contacts_keys = []; 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: '', email: '', contactsLoaded: false, registrationState: null, registrationKeepalive: false, status: null, keys: null, lastSyncId: null, accountVerified: false, autoLogin: false, myContacts: {}, - defaultDomain: config.defaultDomain + defaultDomain: config.defaultDomain, + purgeMessages: [], + updateContactUris: {}, + replicateContacts: {}, + deletedContacts: {} }); this.mustLogout = false; this.changeRoute('/login', 'user logout'); return null; } main() { return null; } } export default Sylk; diff --git a/app/assets/styles/blink/_ReadyBox.scss b/app/assets/styles/blink/_ReadyBox.scss index 8a254ec..44b7ff9 100644 --- a/app/assets/styles/blink/_ReadyBox.scss +++ b/app/assets/styles/blink/_ReadyBox.scss @@ -1,180 +1,179 @@ @import './variables'; .container { flex: 1; flex-direction: column; justify-content: center; } .historyLandscapeContainer { margin-top: 0px; width: 100%; flex: 9; } .historyPortraitContainer { width: 100%; flex: 9; } .landscapeTitle { color: white; font-size: 20px; width: 90%; margin-left: 5%; } .portraitTitle { color: white; font-size: 20px; width: 90%; margin-top: 10px; margin-left: 5%; } .landscapeTabletTitle { margin-top: 20px; color: white; font-size: 24px; width: 100%; margin-left: 3px; } .portraitTabletTitle { margin-top: 20px; color: white; font-size: 20px; width: 100%; margin-left: 10px; } .portraitUriButtonGroup { flex-direction: column; width: 100%; margin-left: 0%; } .landscapeUriButtonGroup { flex-direction: row; width: 100%; margin-left: 2px; justify-content: space-between; } .portraitTabletUriButtonGroup { flex-direction: column; width: 100%; margin-left: 0%; justify-content: space-between; } .landscapeTabletUriButtonGroup { flex-direction: row; width: 100%; margin-left: 0%; justify-content: space-between; } .portraitUriInputBox { align: left; width: 100%; } .landscapeUriInputBox { align: left; flex: 1; padding-top: 10px; margin-left: 2px; margin-right: 8px; } .portraitTabletUriInputBox { align: left; padding-top: 10px; width: 100%; } .landscapeTabletUriInputBox { align: left; padding-top: 10px; width: 66%; margin-left: 3px; } .buttonGroup { flex-direction: row; padding-top: 5px; justify-content: center; } .landscapeButtonGroup { flex: 1; flex-direction: row; justify-content: flex-end; } .uriInputBox { align: left; padding-top: 10px; width: 100%; } .button { background-color: rgba(#6DAA63, .9); margin: 10px; padding-top: 2px; padding-left: 1px; } .iosButton { background-color: rgba(#6DAA63, .9); margin: 10px; padding-top: 4px; padding-left: 1px; } .androidButton { background-color: rgba(#6DAA63, .9); margin: 10px; padding-top: 1px; padding-left: 1px; } .conferenceButton { background-color: rgba(#4572a6, 1); margin: 10px; } .footer { flex: 1; justify-content: flex-end; padding-bottom: 0px; } .backButton { justifyContent: center; alignItems: center; - flex: 1; background-color: red; color: white; margin: 10px; border-radius: 10px; border: 1px; } .navigationContainer { border: 0.5px; } .navigationButtonGroup { justify-content: center; } .navigationButton { } .navigationButtonSelected { background-color: rgba(#6DAA63, .9); } diff --git a/app/components/AudioCallBox.js b/app/components/AudioCallBox.js index 0cd19af..8b89427 100644 --- a/app/components/AudioCallBox.js +++ b/app/components/AudioCallBox.js @@ -1,377 +1,396 @@ import React, { Component } from 'react'; import { View, Platform, TouchableWithoutFeedback } from 'react-native'; import { IconButton, Dialog, Text, ActivityIndicator, Colors } from 'react-native-paper'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import EscalateConferenceModal from './EscalateConferenceModal'; import CallOverlay from './CallOverlay'; import DTMFModal from './DTMFModal'; import UserIcon from './UserIcon'; import styles from '../assets/styles/blink/_AudioCallBox.scss'; import utils from '../utils'; import TrafficStats from './BarChart'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class AudioCallBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { remoteUri : this.props.remoteUri, remoteDisplayName : this.props.remoteDisplayName, photo : this.props.photo, active : false, audioMuted : this.props.muted, showDtmfModal : false, showEscalateConferenceModal : false, call : this.props.call, reconnectingCall : this.props.reconnectingCall, info : this.props.info, packetLossQueue : [], audioBandwidthQueue : [], latencyQueue : [], - declineReason : this.props.declineReason + declineReason : this.props.declineReason, + callContact : this.props.callContact, + selectedContact : this.props.selectedContact }; this.remoteAudio = React.createRef(); this.userHangup = false; } componentDidMount() { // This component is used both for as 'local media' and as the in-call component. // Thus, if the call is not null it means we are beyond the 'local media' phase // so don't call the mediaPlaying prop. if (this.state.call != null) { switch (this.state.call.state) { case 'established': this.attachStream(this.state.call); break; case 'incoming': this.props.mediaPlaying(); // fall through default: this.state.call.on('stateChanged', this.callStateChanged); break; } } } componentWillUnmount() { if (this.state.call != null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.call !== null && nextProps.call !== this.state.call) { if (nextProps.call.state === 'established') { this.attachStream(nextProps.call); this.setState({reconnectingCall: false}); } nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call}); } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.hasOwnProperty('info')) { this.setState({info: nextProps.info}); } if (nextProps.hasOwnProperty('packetLossQueue')) { this.setState({packetLossQueue: nextProps.packetLossQueue}); } if (nextProps.hasOwnProperty('audioBandwidthQueue')) { this.setState({audioBandwidthQueue: nextProps.audioBandwidthQueue}); } if (nextProps.hasOwnProperty('latencyQueue')) { this.setState({latencyQueue: nextProps.latencyQueue}); } this.setState({remoteUri: nextProps.remoteUri, remoteDisplayName: nextProps.remoteDisplayName, photo: nextProps.photo ? nextProps.photo : this.state.photo, - declineReason: nextProps.declineReason + declineReason: nextProps.declineReason, + callContact: nextProps.callContact, + selectedContact: nextProps.selectedContact }); } componentWillUnmount() { if (this.state.call != null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.callTimer); } callStateChanged(oldState, newState, data) { if (newState === 'established') { this.attachStream(this.state.call); this.setState({reconnectingCall: false}); } } attachStream(call) { this.setState({stream: call.getRemoteStreams()[0]}); //we dont use it anywhere though as audio gets automatically piped } escalateToConference(participants) { this.props.escalateToConference(participants); } hangupCall(event) { event.preventDefault(); this.props.hangupCall('user_hangup_call'); this.userHangup = true; } cancelCall(event) { event.preventDefault(); this.props.hangupCall('user_cancel_call'); } muteAudio(event) { event.preventDefault(); this.props.toggleMute(this.props.call.id, !this.state.audioMuted); } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } toggleEscalateConferenceModal() { this.setState({ showEscalateConferenceModal: !this.state.showEscalateConferenceModal }); } handleDoubleTap() { const now = Date.now(); const DOUBLE_PRESS_DELAY = 300; if (this.lastTap && now - this.lastTap < DOUBLE_PRESS_DELAY) { this.props.showLogs(); } else { this.lastTap = now; } } render() { let buttonContainerClass; let userIconContainerClass; let remoteIdentity = {uri: this.state.remoteUri || '', name: this.state.remoteDisplayName || '', photo: this.state.photo }; const username = this.state.remoteUri.split('@')[0]; const isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let displayName = this.state.remoteUri ? toTitleCase(this.state.remoteUri.split('@')[0]) : ''; if (this.state.remoteDisplayName && this.state.remoteUri !== this.state.remoteDisplayName) { displayName = this.state.remoteDisplayName; } if (this.props.isTablet) { buttonContainerClass = this.props.orientation === 'landscape' ? styles.tabletLandscapeButtonContainer : styles.tabletPortraitButtonContainer; userIconContainerClass = styles.tabletUserIconContainer; } else { buttonContainerClass = this.props.orientation === 'landscape' ? styles.landscapeButtonContainer : styles.portraitButtonContainer; userIconContainerClass = styles.userIconContainer; } const buttonSize = this.props.isTablet ? 40 : 34; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; const chatButtonClass = (Platform.OS === 'ios') ? styles.iosChatButton : styles.androidChatButton; + let disablePlus = false; + if (this.state.callContact) { + if (this.state.callContact.tags.indexOf('test') > -1 || this.state.callContact.tags.indexOf('conference') > -1) { + disablePlus = true; + } + } + //console.log('Call to remoteIdentity', remoteIdentity); return ( {displayName} {this.state.remoteUri} {this.props.orientation !== 'landscape' && this.state.reconnectingCall ? : null } {this.state.call && ((this.state.call.state === 'accepted' || this.state.call.state === 'established') && !this.state.reconnectingCall) ? + {!disablePlus ? + : null} + + {!disablePlus ? + : null} {isPhoneNumber ? : null } : } ); } } AudioCallBox.propTypes = { remoteUri : PropTypes.string, remoteDisplayName : PropTypes.string, photo : PropTypes.string, call : PropTypes.object, connection : PropTypes.object, accountId : PropTypes.string, escalateToConference : PropTypes.func, info : PropTypes.string, hangupCall : PropTypes.func, mediaPlaying : PropTypes.func, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool, packetLossQueue : PropTypes.array, videoBandwidthQueue : PropTypes.array, audioBandwidthQueue : PropTypes.array, latencyQueue : PropTypes.array, declineReason : PropTypes.string, showLogs : PropTypes.func, goBackFunc : PropTypes.func, callState : PropTypes.object, messages : PropTypes.object, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, confirmRead : PropTypes.func, deleteMessage : PropTypes.func, expireMessage : PropTypes.func, getMessages : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, + callContact : PropTypes.object, selectedContact : PropTypes.object }; export default AudioCallBox; diff --git a/app/components/Call.js b/app/components/Call.js index 484d863..47445e5 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,1022 +1,1028 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import assert from 'assert'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import uuid from 'react-native-uuid'; import AudioCallBox from './AudioCallBox'; import LocalMedia from './LocalMedia'; import VideoBox from './VideoBox'; import config from '../config'; import utils from '../utils'; function randomIntFromInterval(min,max) { return Math.floor(Math.random()*(max-min+1)+min); } function FixedQueue( size, initialValues ){ // If there are no initial arguments, default it to // an empty value so we can call the constructor in // a uniform way. initialValues = (initialValues || []); // Create the fixed queue array value. var queue = Array.apply( null, initialValues ); // Store the fixed size in the queue. queue.fixedSize = size; // Add the class methods to the queue. Some of these have // to override the native Array methods in order to make // sure the queue lenght is maintained. queue.push = FixedQueue.push; queue.splice = FixedQueue.splice; queue.unshift = FixedQueue.unshift; // Trim any initial excess from the queue. FixedQueue.trimTail.call( queue ); // Return the new queue. return( queue ); } // I trim the queue down to the appropriate size, removing // items from the beginning of the internal array. FixedQueue.trimHead = function(){ // Check to see if any trimming needs to be performed. if (this.length <= this.fixedSize){ // No trimming, return out. return; } // Trim whatever is beyond the fixed size. Array.prototype.splice.call( this, 0, (this.length - this.fixedSize) ); }; // I trim the queue down to the appropriate size, removing // items from the end of the internal array. FixedQueue.trimTail = function(){ // Check to see if any trimming needs to be performed. if (this.length <= this.fixedSize){ // No trimming, return out. return; } // Trim whatever is beyond the fixed size. Array.prototype.splice.call( this, this.fixedSize, (this.length - this.fixedSize) ); }; // I synthesize wrapper methods that call the native Array // methods followed by a trimming method. FixedQueue.wrapMethod = function( methodName, trimMethod ){ // Create a wrapper that calls the given method. var wrapper = function(){ // Get the native Array method. var method = Array.prototype[ methodName ]; // Call the native method first. var result = method.apply( this, arguments ); // Trim the queue now that it's been augmented. trimMethod.call( this ); // Return the original value. return( result ); }; // Return the wrapper method. return( wrapper ); }; // Wrap the native methods. FixedQueue.push = FixedQueue.wrapMethod( "push", FixedQueue.trimHead ); FixedQueue.splice = FixedQueue.wrapMethod( "splice", FixedQueue.trimTail ); FixedQueue.unshift = FixedQueue.wrapMethod( "unshift", FixedQueue.trimTail ); class Call extends Component { constructor(props) { super(props); autoBind(this); this.samples = 30; this.sampleInterval = 3; this.defaultWaitInterval = 90; // until we can connect or reconnect this.waitCounter = 0; this.waitInterval = this.defaultWaitInterval; this.videoBytesSent = 0; this.audioBytesSent = 0; this.videoBytesReceived = 0; this.audioBytesReceived = 0; this.packetLoss = 0; this.packetLossQueue = FixedQueue(this.samples); this.latencyQueue = FixedQueue(this.samples); this.audioBandwidthQueue = FixedQueue(this.samples); this.videoBandwidthQueue = FixedQueue(this.samples); this.mediaLost = false; let callUUID; let remoteUri = ''; let remoteDisplayName = ''; let callState = null; let direction = null; let callEnded = false; this.mediaIsPlaying = false; this.ended = false; this.answering = false; if (this.props.call) { // If current call is available on mount we must have incoming this.props.call.on('stateChanged', this.callStateChanged); remoteUri = this.props.call.remoteIdentity.uri; callState = this.props.call.state; remoteDisplayName = this.props.call.remoteIdentity.displayName || this.props.call.remoteIdentity.uri; direction = this.props.call.direction; callUUID = this.props.call.id; } else { remoteUri = this.props.targetUri; remoteDisplayName = this.props.targetUri; callUUID = this.props.callUUID; direction = callUUID ? 'outgoing' : 'incoming'; } if (this.props.connection) { //console.log('Added listener for connection', this.props.connection); this.props.connection.on('stateChanged', this.connectionStateChanged); } let audioOnly = false; if (this.props.localMedia && this.props.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.state = { call: this.props.call, targetUri: this.props.targetUri, audioOnly: audioOnly, boo: false, remoteUri: remoteUri, remoteDisplayName: remoteDisplayName, localMedia: this.props.localMedia, connection: this.props.connection, accountId: this.props.account ? this.props.account.id : null, account: this.props.account, callState: callState, direction: direction, callUUID: callUUID, reconnectingCall: this.props.reconnectingCall, info: '', packetLossQueue: [], audioBandwidthQueue: [], videoBandwidthQueue: [], latencyQueue: [], declineReason: this.props.declineReason, messages: this.props.messages, - selectedContact: this.props.selectedContact + selectedContact: this.props.selectedContact, + callContact: this.props.callContact } this.statisticsTimer = setInterval(() => { this.getConnectionStats(); }, this.sampleInterval * 1000); } componentDidMount() { this.resetStats(); this.lookupContact(); if (this.state.direction === 'outgoing' && this.state.callUUID && this.state.callState !== 'established') { utils.timestampedLog('Call: start call', this.state.callUUID, 'when ready to', this.state.targetUri); this.startCallWhenReady(this.state.callUUID); } if (this.state.call === null) { this.mediaPlaying(); } } componentWillUnmount() { this.ended = true; this.answering = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } if (this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); } } resetStats() { if (this.ended) { return; } this.setState({ bandwidth: '', packetLossQueue: [], audioBandwidthQueue: [], videoBandwidthQueue: [], latencyQueue: [] }); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { // Needed for switching to incoming call while in a call if (this.ended) { return; } if (nextProps.connection && nextProps.connection !== this.state.connection) { nextProps.connection.on('stateChanged', this.connectionStateChanged); } this.setState({connection: nextProps.connection, account: nextProps.account, call: nextProps.call, + callContact: nextProps.callContact, accountId: nextProps.account ? nextProps.account.id : null}); if (this.state.call === null && nextProps.call !== null) { nextProps.call.on('stateChanged', this.callStateChanged); this.setState({ remoteUri: nextProps.call.remoteIdentity.uri, direction: nextProps.call.direction, callUUID: nextProps.call.id, remoteDisplayName: nextProps.call.remoteIdentity.displayName }); this.lookupContact(); } else { if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) { this.setState({'callUUID': nextProps.callUUID, 'direction': 'outgoing', 'call': null }); this.startCallWhenReady(nextProps.callUUID); } } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.targetUri !== this.state.targetUri && this.state.direction === 'outgoing') { this.setState({targetUri: nextProps.targetUri}); } this.setState({registrationState: nextProps.registrationState, declineReason: nextProps.declineReason}); if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia && this.state.direction === 'outgoing') { utils.timestampedLog('Call: media for outgoing call has been changed'); let audioOnly = false; if (nextProps.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.setState({localMedia: nextProps.localMedia, audioOnly: audioOnly}); //this.mediaPlaying(nextProps.localMedia); } this.setState({messages: nextProps.messages}); } getConnectionStats() { if (this.ended) { return; } let speed = 0; let diff = 0; let delay = 0; let audioPackets = 0; let videoPackets = 0; let audioPacketsLost = 0; let videoPacketsLost = 0; let audioPacketLoss = 0; let videoPacketLoss = 0; let bandwidthUpload = 0; let bandwidthDownload = 0; let mediaType; let foundVideo = false; if (!this.state.call || !this.state.call._pc) { this.resetStats(); return; } this.state.call._pc.getStats(null).then(stats => { stats.forEach(report => { if (report.type === "ssrc") { report.values.forEach(object => { if (object.mediaType) { mediaType = object.mediaType; } }); report.values.forEach(object => { if (object.bytesReceived) { const bytesReceived = Math.floor(object.bytesReceived); if (mediaType === 'audio') { if (this.audioBytesReceived > 0 && this.audioBytesReceived < bytesReceived) { diff = bytesReceived - this.audioBytesReceived; diff = bytesReceived - this.audioBytesReceived; speed = Math.floor(diff / this.sampleInterval * 8 / 1000); //console.log('Audio bandwidth received', speed, 'kbit/s'); bandwidthDownload = bandwidthDownload + speed; if (this.audioBandwidthQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.audioBandwidthQueue.push(0); n = n - 1; } } this.audioBandwidthQueue.push(speed); } this.audioBytesReceived = bytesReceived; } else if (mediaType === 'video') { foundVideo = true; if (this.videoBytesReceived > 0 && this.videoBytesReceived < bytesReceived) { diff = bytesReceived - this.videoBytesReceived; speed = Math.floor(diff / this.sampleInterval * 8 / 1000); //console.log('Video bandwidth received', speed, 'kbit/s'); bandwidthDownload = bandwidthDownload + speed; if (this.videoBandwidthQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.videoBandwidthQueue.push(0); n = n - 1; } } this.videoBandwidthQueue.push(speed) } this.videoBytesReceived = bytesReceived; } } else if (object.bytesSent) { const bytesSent = Math.floor(object.bytesSent); if (mediaType === 'audio') { if (this.audioBytesSent > 0 && bytesSent > this.audioBytesSent) { const diff = bytesSent - this.audioBytesSent; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log('Audio bandwidth sent', speed, 'kbit/s'); } this.audioBytesSent = bytesSent; } else if (mediaType === 'video') { foundVideo = true; if (this.videoBytesSent > 0 && bytesSent > this.videoBytesSent) { const diff = bytesSent - this.videoBytesSent; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log('Video bandwidth sent', speed, 'kbit/s'); } this.videoBytesSent = bytesSent; } } else if (object.packetsLost) { if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsLost); audioPacketsLost = audioPacketsLost + Math.floor(object.packetsLost); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsLost); videoPacketsLost = videoPacketsLost + Math.floor(object.packetsLost); } } else if (object.packetsReceived) { if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsReceived); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsReceived); } } else if (object.googCurrentDelayMs) { delay = object.googCurrentDelayMs; } //console.log(object); }); }}); // packet loss videoPacketLoss = 0; if (videoPackets > 0) { videoPacketLoss = Math.floor(videoPacketsLost / videoPackets * 100); if (videoPacketLoss > 1) { //console.log('Video packet loss', videoPacketLoss, '%'); } } audioPacketLoss = 0; if (audioPackets > 0) { audioPacketLoss = Math.floor(audioPacketsLost / audioPackets * 100); if (audioPacketLoss > 3) { //console.log('Audio packet loss', audioPacketLoss, '%'); } } this.packetLoss = videoPacketLoss > audioPacketLoss ? videoPacketLoss : audioPacketLoss; //this.packetLoss = randomIntFromInterval(2, 10); if (this.packetLoss < 3) { this.packetLoss = 0; } if (this.packetLossQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.packetLossQueue.push(0); n = n - 1; } } if (this.latencyQueue.length < this.samples) { var n = this.samples; while (n > 0) { this.latencyQueue.push(0); n = n - 1; } } this.latencyQueue.push(Math.ceil(delay)); this.packetLossQueue.push(this.packetLoss); this.audioPacketLoss = audioPacketLoss; this.videoPacketLoss = videoPacketLoss; let info = ''; let suffix = 'kbit/s'; if (foundVideo && (bandwidthUpload > 0 || bandwidthDownload > 0)) { suffix = 'Mbit/s'; bandwidthUpload = Math.ceil(bandwidthUpload / 1000 * 100) / 100; bandwidthDownload = Math.ceil(bandwidthDownload / 1000 * 100) / 100; } if (bandwidthDownload && bandwidthUpload) { if (bandwidthDownload > 0 && bandwidthUpload > 0) { info = '⇣' + bandwidthDownload + ' ⇡' + bandwidthUpload; } else if (bandwidthDownload > 0) { info = '⇣' + bandwidthDownload; } else if (bandwidthUpload > 0) { info = '⇡' + this.bandwidthUpload; } if (info) { info = info + ' ' + suffix; } } if (this.packetLoss > 2) { info = info + ' - ' + Math.ceil(this.packetLoss) + '% loss'; } if (delay > 150) { info = info + ' - ' + Math.ceil(delay) + ' ms'; } this.setState({packetLossQueue: this.packetLossQueue, latencyQueue: this.latencyQueue, videoBandwidthQueue: this.videoBandwidthQueue, audioBandwidthQueue: this.audioBandwidthQueue, info: info }); }); }; mediaPlaying(localMedia) { if (this.state.direction === 'incoming') { const media = localMedia ? localMedia : this.state.localMedia; this.answerCall(media); } else { this.mediaIsPlaying = true; } } answerCall(localMedia) { const media = localMedia ? localMedia : this.state.localMedia; if (this.state.call && this.state.call.state === 'incoming' && media) { let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = media; if (!this.answering) { this.answering = true; const connectionState = this.state.connection.state ? this.state.connection.state : null; utils.timestampedLog('Call: answering call', this.state.call.id, 'in connection state', connectionState); try { this.state.call.answer(options); utils.timestampedLog('Call: answered'); } catch (error) { utils.timestampedLog('Call: failed to answer', error); this.hangupCall('answer_failed') } } else { utils.timestampedLog('Call: answering call in progress...'); } } else { if (!this.state.call) { utils.timestampedLog('Call: no Sylkrtc call present'); this.hangupCall('answer_failed'); } if (this.state.call && this.state.call.state !== 'incoming') { utils.timestampedLog('Call: state is not incoming'); } if (!media) { utils.timestampedLog('Call: waiting for local media'); } } } lookupContact() { // TODO this must lookup in myContacts let photo = null; let remoteUri = this.state.remoteUri || ''; let remoteDisplayName = this.state.remoteDisplayName || ''; if (!remoteUri) { return; } if (remoteUri.indexOf('3333@') > -1) { remoteDisplayName = 'Video Test'; } else if (remoteUri.indexOf('4444@') > -1) { remoteDisplayName = 'Echo Test'; } else if (this.props.myContacts.hasOwnProperty(remoteUri) && this.props.myContacts[remoteUri].name) { remoteDisplayName = this.props.myContacts[remoteUri].name; } else if (this.props.contacts) { let username = remoteUri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { var contact_obj = this.findObjectByKey(this.props.contacts, 'uri', username); } else { var contact_obj = this.findObjectByKey(this.props.contacts, 'uri', remoteUri); } if (contact_obj) { remoteDisplayName = contact_obj.displayName; photo = contact_obj.photo; if (isPhoneNumber) { remoteUri = username; } } else { if (isPhoneNumber) { remoteUri = username; remoteDisplayName = username; } } } this.setState({remoteDisplayName: remoteDisplayName, remoteUri: remoteUri, photo: photo }); } callStateChanged(oldState, newState, data) { //console.log('Call: callStateChanged', oldState, '->', newState); if (this.ended) { return; } let remoteHasNoVideoTracks; let remoteIsRecvOnly; let remoteIsInactive; let remoteStreams; this.answering = false; if (newState === 'established') { this.setState({reconnectingCall: false}); const currentCall = this.state.call; if (currentCall) { remoteStreams = currentCall.getRemoteStreams(); if (remoteStreams) { if (remoteStreams.length > 0) { const remotestream = remoteStreams[0]; remoteHasNoVideoTracks = remotestream.getVideoTracks().length === 0; remoteIsRecvOnly = currentCall.remoteMediaDirections.video[0] === 'recvonly'; remoteIsInactive = currentCall.remoteMediaDirections.video[0] === 'inactive'; } } } if (remoteStreams && (remoteHasNoVideoTracks || remoteIsRecvOnly || remoteIsInactive) && !this.state.audioOnly) { //console.log('Media type changed to audio'); // Stop local video if (this.state.localMedia.getVideoTracks().length !== 0) { currentCall.getLocalStreams()[0].getVideoTracks()[0].stop(); } this.setState({audioOnly: true}); } else { this.forceUpdate(); } } else if (newState === 'accepted') { // Switch if we have audioOnly and local videotracks. This means // the call object switched and we are transitioning to an // incoming call. if (this.state.audioOnly && this.state.localMedia && this.state.localMedia.getVideoTracks().length !== 0) { //console.log('Media type changed to video on accepted'); this.setState({audioOnly: false}); } } this.forceUpdate(); } connectionStateChanged(oldState, newState) { switch (newState) { case 'closed': break; case 'ready': break; case 'disconnected': if (oldState === 'ready' && this.state.direction === 'outgoing') { utils.timestampedLog('Call: reconnecting the call...'); this.waitInterval = this.defaultWaitInterval; } break; default: break; } } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } canConnect() { if (!this.state.connection) { console.log('Call: no connection yet'); return false; } if (this.state.connection.state !== 'ready') { console.log('Call: connection is not ready'); return false; } if (this.props.registrationState !== 'registered') { console.log('Call: account not ready yet'); return false; } if (!this.mediaIsPlaying) { if (this.waitCounter > 0) { console.log('Call: media is not yet playing'); } return false; } return true; } async startCallWhenReady(callUUID) { this.waitCounter = 0; let diff = 0; while (this.waitCounter < this.waitInterval) { if (this.waitCounter === 1) { utils.timestampedLog('Call: waiting for establishing call', this.waitInterval, 'seconds'); } if (this.userHangup) { this.hangupCall('user_cancelled'); return; } if (this.ended) { return; } if (this.waitCounter >= this.waitInterval - 1) { this.hangupCall('timeout'); } if (!this.canConnect()) { //utils.timestampedLog('Call: waiting for connection', this.waitInterval - this.waitCounter, 'seconds'); if (this.state.call && this.state.call.id === callUUID && this.state.call.state !== 'terminated') { return; } if (this.waitCounter > 0 && this.waitCounter % 10 === 0) { console.log('Wait', this.waitCounter); } await this._sleep(1000); } else { this.waitCounter = 0; this.start(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } start() { if (this.state.localMedia === null) { console.log('Call: cannot create new call without local media'); return; } let options = {pcConfig: {iceServers: config.iceServers}, id: this.state.callUUID}; options.localStream = this.state.localMedia; let call = this.state.account.call(this.state.targetUri, options); if (call) { call.on('stateChanged', this.callStateChanged); this.setState({call: call}); } } hangupCall(reason) { let callUUID = this.state.call ? this.state.call.id : this.state.callUUID; this.waitInterval = this.defaultWaitInterval; if (this.state.call) { //console.log('Remove listener for call', this.state.call.id); this.state.call.removeListener('stateChanged', this.callStateChanged); this.setState({call: null}); } if (this.state.connection) { //console.log('Remove listener for connection', this.state.connection); this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.setState({connection: null}); } if (this.waitCounter > 0) { this.waitCounter = this.waitInterval; } this.props.hangupCall(callUUID, reason); } render() { let box = null; if (this.state.localMedia !== null) { if (this.state.audioOnly) { box = ( ); } else { if (this.state.call !== null && (this.state.call.state === 'established' || (this.state.call.state === 'terminated' && this.state.reconnectingCall))) { box = ( ); } else { if (this.state.call && this.state.call.state === 'terminated' && this.state.reconnectingCall) { //console.log('Skip render local media because we will reconnect'); } else { box = ( ); } } } } else { box = ( ); } return box; } } Call.propTypes = { targetUri : PropTypes.string, account : PropTypes.object, hangupCall : PropTypes.func, connection : PropTypes.object, registrationState : PropTypes.string, call : PropTypes.object, localMedia : PropTypes.object, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, callUUID : PropTypes.string, contacts : PropTypes.array, intercomDtmfTone : PropTypes.string, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool, myContacts : PropTypes.object, declineReason : PropTypes.string, showLogs : PropTypes.func, goBackFunc : PropTypes.func, callState : PropTypes.object, messages : PropTypes.object, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, confirmRead : PropTypes.func, deleteMessage : PropTypes.func, expireMessage : PropTypes.func, getMessages : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, - selectedContact : PropTypes.object + selectedContact : PropTypes.object, + callContact : PropTypes.object }; export default Call; diff --git a/app/components/ContactsListBox.js b/app/components/ContactsListBox.js index f24877b..e1f8001 100644 --- a/app/components/ContactsListBox.js +++ b/app/components/ContactsListBox.js @@ -1,933 +1,935 @@ import React, { Component} from 'react'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import { Clipboard, SafeAreaView, View, FlatList, Text } from 'react-native'; import ContactCard from './ContactCard'; import utils from '../utils'; import DigestAuthRequest from 'digest-auth-request'; import uuid from 'react-native-uuid'; import { GiftedChat, IMessage, Bubble } from 'react-native-gifted-chat' import MessageInfoModal from './MessageInfoModal'; import ShareMessageModal from './ShareMessageModal'; import CustomChatActions from './ChatActions'; import moment from 'moment'; import momenttz from 'moment-timezone'; import styles from '../assets/styles/blink/_ContactsListBox.scss'; class ContactsListBox extends Component { constructor(props) { super(props); autoBind(this); this.chatListRef = React.createRef(); this.state = { accountId: this.props.account ? this.props.account.id : null, password: this.props.password, targetUri: this.props.selectedContact ? this.props.selectedContact.uri : this.props.targetUri, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, isRefreshing: false, isLandscape: this.props.isLandscape, contacts: this.props.contacts, myInvitedParties: this.props.myInvitedParties, refreshHistory: this.props.refreshHistory, selectedContact: this.props.selectedContact, myContacts: this.props.myContacts, messages: this.props.messages, renderMessages: [], chat: this.props.chat, pinned: false, showMessageModal: false, message: null, showShareMessageModal: false, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectedContacts: this.props.selectedContacts, pinned: this.props.pinned, filter: this.props.filter, periodFilter: this.props.periodFilter, scrollToBottom: true, messageZoomFactor: this.props.messageZoomFactor, isTyping: false, isLoadingEarlier: false } this.ended = false; } componentDidMount() { this.ended = false; } componentWillUnmount() { this.ended = true; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (this.ended) { return; } if (nextProps.myInvitedParties !== this.state.myInvitedParties) { this.setState({myInvitedParties: nextProps.myInvitedParties}); } if (nextProps.contacts !== this.state.contacts) { this.setState({contacts: nextProps.contacts}); } if (nextProps.favoriteUris !== this.state.favoriteUris) { this.setState({favoriteUris: nextProps.favoriteUris}); } if (nextProps.blockedUris !== this.state.blockedUris) { this.setState({blockedUris: nextProps.blockedUris}); } if (nextProps.account !== null && nextProps.account !== this.props.account) { this.setState({accountId: nextProps.account.id}); } if (nextProps.refreshHistory !== this.state.refreshHistory) { this.setState({refreshHistory: nextProps.refreshHistory}); this.getServerHistory(); } if (nextProps.messageZoomFactor !== this.state.messageZoomFactor) { this.setState({scrollToBottom: false, messageZoomFactor: nextProps.messageZoomFactor}); } if (nextProps.selectedContact !== this.state.selectedContact) { this.setState({selectedContact: nextProps.selectedContact}); if (nextProps.selectedContact) { this.setState({scrollToBottom: true}); this.getMessages(nextProps.selectedContact); } }; if (nextProps.myContacts !== this.state.myContacts) { this.setState({myContacts: nextProps.myContacts}); }; if (this.state.messages) { let renderMessages = []; if (this.state.selectedContact) { let uri = this.state.selectedContact.uri; if (nextProps.messages && nextProps.messages.hasOwnProperty(uri)) { renderMessages = nextProps.messages[uri]; if (this.state.renderMessages.length !== renderMessages.length) { this.setState({isLoadingEarlier: false}); this.props.confirmRead(uri); if (this.state.renderMessages.length > 0 && renderMessages.length > 0) { let last_message_ts = this.state.renderMessages[0].createdAt; if (renderMessages[0].createdAt > last_message_ts) { this.setState({scrollToBottom: true}); } } } } if (renderMessages !== this.state.renderMessages) { renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); this.setState({renderMessages: GiftedChat.append(renderMessages, [])}); if (!this.state.scrollToBottom && renderMessages.length > 0) { //console.log('Scroll to first message'); //this.scrollToMessage(0); } } } } else { this.setState({renderMessages: []}); } this.setState({isLandscape: nextProps.isLandscape, chat: nextProps.chat, filter: nextProps.filter, password: nextProps.password, showMessageModal: nextProps.showMessageModal, message: nextProps.message, inviteContacts: nextProps.inviteContacts, shareToContacts: nextProps.shareToContacts, selectedContacts: nextProps.selectedContacts, pinned: nextProps.pinned, isTyping: nextProps.isTyping, periodFilter: nextProps.periodFilter, targetUri: nextProps.selectedContact ? nextProps.selectedContact.uri : nextProps.targetUri }); if (nextProps.isTyping) { setTimeout(() => { this.setState({isTyping: false}); }, 3000); } } renderCustomActions = props => ( ) onSendFromUser() { console.log('On send from user...'); } getMessages(contact) { if (!contact) { return; } this.props.getMessages(contact.uri); } setTargetUri(uri, contact) { //console.log('Set target uri uri in history list', uri); this.props.setTargetUri(uri, contact); } setFavoriteUri(uri) { return this.props.setFavoriteUri(uri); } setBlockedUri(uri) { return this.props.setBlockedUri(uri); } renderItem(object) { let item = object.item || object; let invitedParties = []; let uri = item.uri; let myDisplayName; let username = uri.split('@')[0]; if (this.state.myContacts && this.state.myContacts.hasOwnProperty(uri)) { myDisplayName = this.state.myContacts[uri].name; } if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(username)) { invitedParties = this.state.myInvitedParties[username]; } if (myDisplayName) { if (item.name === item.uri || item.name !== myDisplayName) { item.name = myDisplayName; } } return( ); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } closeMessageModal() { this.setState({showMessageModal: false, message: null}); } loadEarlierMessages() { //console.log('Load earlier messages...'); this.setState({scrollToBottom: false, isLoadingEarlier: true}); this.props.loadEarlierMessages(); } onSendWithFile(selectedFile) { let uri; if (!this.state.selectedContact) { if (this.state.targetUri && this.state.chat) { let contacts = this.searchedContact(this.state.targetUri); if (contacts.length !== 1) { return; } uri = contacts[0].uri; } else { return; } } else { uri = this.state.selectedContact.uri; } let fileData = { name: selectedFile.name, type: selectedFile.type, size: selectedFile.size, uri: selectedFile.uri }; console.log('Sending file', fileData); //this.props.sendMessage(uri, message); } onSendMessage(messages) { let uri; if (!this.state.selectedContact) { if (this.state.targetUri && this.state.chat) { let contacts = this.searchedContact(this.state.targetUri); if (contacts.length !== 1) { return; } uri = contacts[0].uri; } else { return; } } else { uri = this.state.selectedContact.uri; } messages.forEach((message) => { /* sent: true, // Mark the message as received, using two tick received: true, // Mark the message as pending with a clock loader pending: true, */ this.props.sendMessage(uri, message); }); let renderMessages = this.state.renderMessages; renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); Array.prototype.push.apply(messages, renderMessages); this.setState({renderMessages: GiftedChat.append(messages, [])}); } searchedContact(uri, contact=null) { - let contacts = []; - if (uri.indexOf(' ') > -1) { - return contacts; + return []; } const item = this.props.newContactFunc(uri.toLowerCase(), null, {src: 'search_contact'}); + if (!item) { + return []; + } + if (contact) { item.name = contact.name; item.photo = contact.photo; } - item.tags.push('synthetic'); - contacts.push(item); - return contacts; + return [item]; } getServerHistory() { if (!this.state.accountId) { return; } + if (this.ended || !this.state.accountId || this.state.isRefreshing) { return; } + console.log('Get server history...'); + this.setState({isRefreshing: true}); let history = []; let localTime; let getServerCallHistory = new DigestAuthRequest( 'GET', `${this.props.config.serverCallHistoryUrl}?action=get_history&realm=${this.state.accountId.split('@')[1]}`, this.state.accountId.split('@')[0], this.state.password ); // Disable logging getServerCallHistory.loggingOn = false; getServerCallHistory.request((data) => { if (data.success !== undefined && data.success === false) { console.log('Error getting call history from server', data.error_message); return; } if (data.received) { data.received.map(elem => {elem.direction = 'incoming'; return elem}); history = history.concat(data.received); } if (data.placed) { data.placed.map(elem => {elem.direction = 'outgoing'; return elem}); history = history.concat(data.placed); } history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) if (history) { const known = []; history = history.filter((elem) => { elem.conference = false; elem.id = uuid.v4(); if (!elem.tags) { elem.tags = []; } if (elem.remoteParty.indexOf('@conference.') > -1) { return null; } if (known.indexOf(elem.uri) > -1) { return null; } known.push(elem.uri); elem.uri = elem.remoteParty.toLowerCase(); let username = elem.uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let contact_obj; if (elem.displayName) { elem.name = elem.displayName; } else { elem.name = elem.uri; } if (this.state.contacts) { if (isPhoneNumber) { contact_obj = this.findObjectByKey(this.state.contacts, 'uri', username); } else { contact_obj = this.findObjectByKey(this.state.contacts, 'uri', elem.uri); } if (contact_obj) { elem.name = contact_obj.name; elem.photo = contact_obj.photo; elem.label = contact_obj.label; if (isPhoneNumber) { elem.uri = username; } // TODO update icon here } else { elem.photo = null; } } if (elem.uri.indexOf('@guest.') > -1) { elem.uri = elem.name.toLowerCase().replace(/ /g, '') + '@' + elem.uri.split('@')[1]; } if (elem.remoteParty.indexOf('@videoconference.') > -1) { elem.conference = true; elem.media = ['audio', 'video', 'chat']; } if (elem.uri === this.state.accountId) { elem.name = this.props.myDisplayName || 'Myself'; } if (!elem.media || !Array.isArray(elem.media)) { elem.media = ['audio']; } if (elem.timezone !== undefined) { localTime = momenttz.tz(elem.startTime, elem.timezone).toDate(); elem.startTime = localTime; elem.timestamp = localTime; localTime = momenttz.tz(elem.stopTime, elem.timezone).toDate(); elem.stopTime = localTime; } if (elem.direction === 'incoming' && elem.duration === 0) { elem.tags.push('missed'); } return elem; }); this.props.saveHistory(history); if (this.ended) { return; } this.setState({isRefreshing: false}); } }, (errorCode) => { console.log('Error getting call history from server', errorCode); }); this.setState({isRefreshing: false}); } matchContact(contact, filter='', tags=[]) { if (!contact) { return false; } if (tags.indexOf('conference') > -1 && contact.conference) { return true; } if (tags.length > 0 && !tags.some(item => contact.tags.includes(item))) { return false; } if (contact.name && contact.name.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return true; } if (contact.uri.toLowerCase().startsWith(filter.toLowerCase())) { return true; } if (!this.state.selectedContact && contact.conference && contact.metadata && filter.length > 2 && contact.metadata.indexOf(filter) > -1) { return true; } return false; } noChatInputToolbar () { return null; } onLongMessagePress(context, currentMessage) { if (currentMessage && currentMessage.text) { let options = [] options.push('Copy'); options.push('Delete'); const showResend = currentMessage.failed; if (this.state.targetUri.indexOf('@videoconference') === -1) { if (currentMessage.direction === 'outgoing') { if (showResend) { options.push('Resend') } } } if (currentMessage.pinned) { options.push('Unpin'); } else { options.push('Pin'); } options.push('Info'); options.push('Share'); options.push('Cancel'); let l = options.length - 1; context.actionSheet().showActionSheetWithOptions({options, l}, (buttonIndex) => { let action = options[buttonIndex]; - console.log('Message action', action); if (action === 'Copy') { Clipboard.setString(currentMessage.text); } else if (action === 'Delete') { this.props.deleteMessage(currentMessage._id, this.state.targetUri); } else if (action === 'Pin') { this.props.pinMessage(currentMessage._id); } else if (action === 'Unpin') { this.props.unpinMessage(currentMessage._id); } else if (action === 'Info') { this.setState({message: currentMessage, showMessageModal: true}); } else if (action === 'Share') { this.setState({message: currentMessage, showShareMessageModal: true}); } else if (action === 'Resend') { this.props.reSendMessage(currentMessage, this.state.targetUri); } }); } }; shouldUpdateMessage(props, nextProps) { return true; } toggleShareMessageModal() { this.setState({showShareMessageModal: !this.state.showShareMessageModal}); } renderMessageBubble (props) { let rightColor = '#0084ff'; let leftColor = '#f0f0f0'; if (props.currentMessage.failed) { rightColor = 'red'; } else { if (props.currentMessage.pinned) { rightColor = '#2ecc71'; leftColor = '#2ecc71'; } } return ( ) } scrollToMessage(id) { //console.log('scrollToMessage', id); //https://github.com/FaridSafi/react-native-gifted-chat/issues/938 this.chatListRef.current?._messageContainerRef?.current?.scrollToIndex({ animated: true, index: id }); } get showChat() { if (this.props.selectedContact && this.props.selectedContact.tags && this.props.selectedContact.tags.indexOf('blocked') > -1) { return false; } let username = this.state.targetUri ? this.state.targetUri.split('@')[0] : null; let isPhoneNumber = username ? username.match(/^(\+|0)(\d+)$/) : false; if (isPhoneNumber) { return false; } if (this.props.selectedContact) { return true; } return false; } render() { let searchExtraItems = []; let items = []; let matchedContacts = []; let messages = this.state.renderMessages; let contacts = []; Object.keys(this.state.myContacts).forEach((uri) => { contacts.push(this.state.myContacts[uri]); }); //console.log('--- Render contacts with selected contact', this.state.selectedContact ? this.state.selectedContact.uri: null); //console.log('--- Render contacts with filter', this.state.filter, 's c', this.state.selectedContact, this.state.inviteContacts); let chatInputClass; if (this.state.selectedContact && this.state.selectedContact.uri.indexOf('@videoconference') > -1) { chatInputClass = this.noChatInputToolbar; } else if (!this.state.chat) { chatInputClass = this.noChatInputToolbar; } if (!this.state.selectedContact && this.state.filter) { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri, [this.state.filter])); } else { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri)); searchExtraItems = searchExtraItems.concat(this.state.contacts); if (this.state.targetUri && this.state.targetUri.length > 2 && !this.state.selectedContact && !this.state.inviteContacts) { matchedContacts = searchExtraItems.filter(contact => this.matchContact(contact, this.state.targetUri)); } else if (this.state.selectedContact && this.state.selectedContact.type === 'contact') { matchedContacts.push(this.state.selectedContact); } else if (this.state.selectedContact) { items = [this.state.selectedContact]; } items = items.concat(matchedContacts); } if (this.state.targetUri) { items = items.concat(this.searchedContact(this.state.targetUri, this.state.selectedContact)); } if (this.state.filter && this.state.targetUri) { items = contacts.filter(contact => this.matchContact(contact, this.state.targetUri)); } const known = []; items = items.filter((elem) => { if (this.state.shareToContacts && elem.tags.indexOf('test') > -1) { return; } if (this.state.inviteContacts && elem.tags.indexOf('conference') > -1 ) { return; } if (this.state.shareToContacts && elem.tags.indexOf('chat') === -1) { return; } if (this.state.accountId === elem.uri && elem.tags.length === 0) { return; } if (this.state.shareToContacts && elem.uri.indexOf('@') === -1) { return; } if (known.indexOf(elem.uri) <= -1) { known.push(elem.uri); return elem; } }); items.forEach((item) => { item.showActions = false; if (item.uri.indexOf('@videoconference.') === -1) { item.conference = false; } else { item.conference = true; } if (this.state.selectedContacts && this.state.selectedContacts.indexOf(item.uri) > -1) { item.selected = true; } else { item.selected = false; } }); let filteredItems = []; items.reverse(); var todayStart = new Date(); todayStart.setHours(0,0,0,0); var yesterdayStart = new Date(); yesterdayStart.setDate(todayStart.getDate() - 2); yesterdayStart.setHours(0,0,0,0); items.forEach((item) => { const fromDomain = '@' + item.uri.split('@')[1]; if (this.state.periodFilter === 'today') { if(item.timestamp < todayStart) { return; } } if (this.state.periodFilter === 'yesterday') { if(item.timestamp < yesterdayStart || item.timestamp > todayStart) { return; } } if (this.state.inviteContacts && item.uri.indexOf('@videoconference.') > -1) { return; } if (item.uri === this.state.accountId && !item.direction) { return; } if (this.state.filter && item.tags.indexOf(this.state.filter) > -1) { filteredItems.push(item); } else if (this.state.blockedUris.indexOf(item.uri) === -1 && this.state.blockedUris.indexOf(fromDomain) === -1) { filteredItems.push(item); } //console.log(item.timestamp, item.type, item.uri); }); items = filteredItems; items.sort((a, b) => (a.timestamp < b.timestamp) ? 1 : -1) if (items.length === 1) { //console.log(items[0]); items[0].showActions = true; } let columns = 1; if (this.props.isTablet) { columns = this.props.orientation === 'landscape' ? 3 : 2; } else { columns = this.props.orientation === 'landscape' ? 2 : 1; } const chatContainer = this.props.orientation === 'landscape' ? styles.chatLandscapeContainer : styles.chatPortraitContainer; const container = this.props.orientation === 'landscape' ? styles.landscapeContainer : styles.portraitContainer; const contactsContainer = this.props.orientation === 'landscape' ? styles.contactsLandscapeContainer : styles.contactsPortraitContainer; const borderClass = (messages.length > 0 && !this.state.chat) ? styles.chatBorder : null; if (items.length === 1) { if (items[0].tags.toString() === 'synthetic') { messages = []; } } let pinned_messages = [] if (this.state.pinned) { messages.forEach((m) => { if (m.pinned) { pinned_messages.push(m); } }); messages = pinned_messages; if (pinned_messages.length === 0) { let msg = { _id: uuid.v4(), text: 'No pinned messages found. Touch individual messages to pin them.', system: true } pinned_messages.push(msg); } } let showLoadEarlier = (this.state.myContacts && this.state.selectedContact && this.state.selectedContact.uri in this.state.myContacts && this.state.myContacts[this.state.selectedContact.uri].totalMessages && this.state.myContacts[this.state.selectedContact.uri].totalMessages > messages.length) ? true: false; return ( {items.length === 1 ? (this.renderItem(items[0])) : item.id} key={this.props.orientation} loadEarlier /> } {this.showChat && ! this.state.inviteContacts? : (items.length === 1) ? { return null }} renderBubble={this.renderBubble} onSend={this.onSendMessage} onLongPress={this.onLongMessagePress} shouldUpdateMessage={this.shouldUpdateMessage} onPress={this.onLongMessagePress} scrollToBottom={this.state.scrollToBottom} inverted={true} timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} infiniteScroll loadEarlier={showLoadEarlier} onLoadEarlier={this.loadEarlierMessages} /> : null } ); } } ContactsListBox.propTypes = { account : PropTypes.object, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, targetUri : PropTypes.string, selectedContact : PropTypes.object, contacts : PropTypes.array, chat : PropTypes.bool, orientation : PropTypes.string, setTargetUri : PropTypes.func, isTablet : PropTypes.bool, isLandscape : PropTypes.bool, refreshHistory : PropTypes.bool, saveHistory : PropTypes.func, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, setFavoriteUri : PropTypes.func, saveConference: PropTypes.func, myInvitedParties: PropTypes.object, setBlockedUri : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, filter : PropTypes.string, periodFilter : PropTypes.string, defaultDomain : PropTypes.string, saveContact : PropTypes.func, myContacts : PropTypes.object, messages : PropTypes.object, getMessages : PropTypes.func, confirmRead : PropTypes.func, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, deleteMessage : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, deleteMessages : PropTypes.func, sendPublicKey : PropTypes.func, inviteContacts : PropTypes.bool, shareToContacts : PropTypes.bool, selectedContacts: PropTypes.array, toggleBlocked : PropTypes.func, togglePinned : PropTypes.func, loadEarlierMessages: PropTypes.func, newContactFunc : PropTypes.func, messageZoomFactor: PropTypes.string, isTyping: PropTypes.bool }; export default ContactsListBox; diff --git a/app/components/EditContactModal.js b/app/components/EditContactModal.js index 87edc54..0b487fc 100644 --- a/app/components/EditContactModal.js +++ b/app/components/EditContactModal.js @@ -1,230 +1,231 @@ 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, Subheading } from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import styles from '../assets/styles/blink/_EditContactModal.scss'; import utils from '../utils'; 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 email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; - return email_reg.test(this.state.email); + + 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} ); } } EditContactModal.propTypes = { show : PropTypes.bool.isRequired, 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 }; export default EditContactModal; diff --git a/app/components/NavigationBar.js b/app/components/NavigationBar.js index 75415ce..4a0d218 100644 --- a/app/components/NavigationBar.js +++ b/app/components/NavigationBar.js @@ -1,553 +1,553 @@ 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'; 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, 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, pinned: this.props.pinned } 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, pinned: nextProps.pinned, menuVisible: nextProps.menuVisible, inCall: nextProps.inCall, publicKey: nextProps.publicKey, showDeleteHistoryModal: nextProps.showDeleteHistoryModal, selectedContact: nextProps.selectedContact, messages: nextProps.messages, showCallMeMaybeModal: nextProps.showCallMeMaybeModal }); } handleMenu(event) { this.callUrl = `${config.publicUrl}/call/${this.state.accountId}`; switch (event) { case 'about': this.toggleAboutModal(); break; case 'callMeMaybe': this.props.toggleCallMeMaybeModal(); break; case 'displayName': this.toggleEditContactModal(); break; case 'speakerphone': this.props.toggleSpeakerPhone(); break; case 'proximity': this.props.toggleProximity(); break; case 'logOut': this.props.logout(); break; case 'logs': this.props.showLogs(); break; case 'preview': this.props.preview(); break; case 'audio': this.audioCall(); break; case 'video': this.videoCall(); 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 'togglePinned': this.props.togglePinned(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(); } 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) { + 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}); } 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}); } 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 = false; 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; } let blockedTitle = (this.state.selectedContact && this.state.selectedContact.tags && this.state.selectedContact.tags.indexOf('blocked') > -1) ? 'Unblock' : 'Block'; let favoriteTitle = (this.state.selectedContact && this.state.selectedContact.tags && this.state.selectedContact.tags.indexOf('favorite') > -1) ? 'Unfavorite' : 'Favorite'; let favoriteIcon = (this.state.selectedContact && this.state.selectedContact.tags && this.state.selectedContact.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 updateTitle = (this.state.appStoreVersion && this.state.appStoreVersion.version > VersionNumber.appVersion) ? 'Update Sylk...' : 'Check for updates...'; return ( {this.state.selectedContact? {this.props.goBackFunc()}} /> : } {this.props.isTablet? {subtitle} : null} {statusIcon ? : null } { this.state.selectedContact ? this.setState({menuVisible: !this.state.menuVisible})} anchor={ this.setState({menuVisible: !this.state.menuVisible})} /> } > this.handleMenu('editContact')} icon="account" title="Edit..."/> {!isConference && !this.state.inCall ? this.handleMenu('audio')} icon="phone" title="Audio call"/> :null} {!isConference && !this.state.inCall ? this.handleMenu('video')} icon="video" title="Video call"/> :null} {isConference && !this.state.inCall ? this.handleMenu('conference')} icon="account-group" title="Join conference..."/> :null} { hasMessages && !this.state.inCall ? this.handleMenu('deleteMessages')} icon="delete" title="Delete messages..."/> : null } { !hasMessages && tags.indexOf('test') === -1 ? this.handleMenu('deleteMessages')} icon="delete" title="Delete contact..."/> : null} { (hasMessages || this.state.pinned) && tags.indexOf('test') === -1 ? this.handleMenu('togglePinned')} icon="pin" title={this.state.pinned ? "Show all messages" : "Show pinned messages"}/> : 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 ? this.handleMenu('toggleFavorite')} icon={favoriteIcon} title={favoriteTitle}/> : null} {tags.indexOf('test') === -1 && tags.indexOf('favorite') === -1 && !this.state.inCall ? this.handleMenu('toggleBlocked')} icon="block-helper" title={blockedTitle}/> : 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} {!this.state.inCall ? this.handleMenu('checkUpdate')} icon="update" title={updateTitle} /> :null} {!this.state.inCall ? this.handleMenu('deleteMessages')} icon="delete" title="Wipe device..."/> :null} {extraMenu ? this.handleMenu('settings')} icon="wrench" title="Server settings..." /> this.handleMenu('logs')} icon="timeline-text-outline" title="Show logs" /> this.handleMenu('proximity')} icon={proximityIcon} title={proximityTitle} /> : null} {!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, togglePinned : PropTypes.func, pinned : PropTypes.bool, 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, 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, showExportPrivateKeyModal: PropTypes.bool, showExportPrivateKeyModalFunc: PropTypes.func, hideExportPrivateKeyModalFunc: PropTypes.func }; export default NavigationBar; diff --git a/app/components/ReadyBox.js b/app/components/ReadyBox.js index 6a937d6..9af22ac 100644 --- a/app/components/ReadyBox.js +++ b/app/components/ReadyBox.js @@ -1,764 +1,761 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import autoBind from 'auto-bind'; import { FlatList, View, Platform} from 'react-native'; import { IconButton, Title, Button } from 'react-native-paper'; import ConferenceModal from './ConferenceModal'; import ContactsListBox from './ContactsListBox'; import FooterBox from './FooterBox'; import URIInput from './URIInput'; import config from '../config'; import utils from '../utils'; import styles from '../assets/styles/blink/_ReadyBox.scss'; import {Keyboard} from 'react-native'; class ReadyBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { targetUri: '', contacts: this.props.contacts, selectedContact: this.props.selectedContact, showConferenceModal: this.props.showConferenceModal, sticky: false, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, historyCategoryFilter: null, historyPeriodFilter: null, missedCalls: this.props.missedCalls, isLandscape: this.props.isLandscape, participants: null, myInvitedParties: this.props.myInvitedParties, messages: this.props.messages, myDisplayName: this.props.myDisplayName, chat: (this.props.selectedContact !== null) && (this.props.call !== null), call: this.props.call, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectedContacts: this.props.selectedContacts, pinned: this.props.pinned, messageZoomFactor: this.props.messageZoomFactor, isTyping: this.props.isTyping, navigationItems: this.props.navigationItems }; this.ended = false; } UNSAFE_componentWillReceiveProps(nextProps) { if (this.ended) { return; } if (this.state.selectedContact && nextProps.selectedContact === null) { this.setState({targetUri: '', chat: false}); } if (!this.state.inviteContacts && nextProps.inviteContacts) { this.handleTargetChange(''); this.setState({chat: false}); } if (this.state.selectedContact !== nextProps.selectedContact && nextProps.selectedContact) { this.setState({chat: !this.chatDisabledForUri(nextProps.selectedContact.uri)}); } if (nextProps.missedCalls.length === 0 && this.state.historyCategoryFilter === 'missed') { this.setState({'historyCategoryFilter': null}); } if (nextProps.blockedUris.length === 0 && this.state.historyCategoryFilter === 'blocked') { this.setState({'historyCategoryFilter': null}); } if (nextProps.favoriteUris.length === 0 && this.state.historyCategoryFilter === 'favorite') { this.setState({'historyCategoryFilter': null}); } this.setState({myInvitedParties: nextProps.myInvitedParties, messages: nextProps.messages, myDisplayName: nextProps.myDisplayName, call: nextProps.call, showConferenceModal: nextProps.showConferenceModal, isTyping: nextProps.isTyping, navigationItems: nextProps.navigationItems, messageZoomFactor: nextProps.messageZoomFactor, contacts: nextProps.contacts, inviteContacts: nextProps.inviteContacts, shareToContacts: nextProps.shareToContacts, selectedContacts: nextProps.selectedContacts, selectedContact: nextProps.selectedContact, pinned: nextProps.pinned, favoriteUris: nextProps.favoriteUris, blockedUris: nextProps.blockedUris, missedCalls: nextProps.missedCalls, isLandscape: nextProps.isLandscape}); } getTargetUri(uri) { return utils.normalizeUri(uri, this.props.defaultDomain); } async componentDidMount() { this.ended = false; } componentWillUnmount() { this.ended = true; } filterHistory(filter) { if (this.ended) { return; } if (!filter) { this.setState({'historyPeriodFilter': null, historyCategoryFilter: null}); } else if (filter === 'today' || filter === 'yesterday') { filter = this.state.historyPeriodFilter === filter ? null : filter; this.setState({'historyPeriodFilter': filter}); } else { this.setState({'historyCategoryFilter': filter}); } this.handleTargetChange(''); } chatDisabledForUri(uri) { if (uri.indexOf('@videoconference') > -1) { return true; } if (uri.indexOf('@guest') > -1) { return true; } if (uri.indexOf('3333@') > -1) { return true; } if (uri.indexOf('4444@') > -1) { return true; } return false; } get showSearchBar() { if (this.props.isTablet || this.props.isLandscape) { return true; } if (this.state.call) { return false; } return (this.state.selectedContact === null); } get showButtonsBar() { if (this.props.isTablet) { return true; } if (this.state.shareToContacts) { return true; } if (this.props.isLandscape) { return true; } if (this.state.call) { return true; } if (!this.state.targetUri) { return true; } if (this.state.selectedContact) { return true; } return true; } handleTargetChange(new_uri, contact) { //console.log('---handleTargetChange new_uri =', new_uri); //console.log('handleTargetChange contact =', contact); if ((this.state.inviteContacts || this.state.shareToContacts) && contact) { const uri = contact.uri; this.props.updateSelection(uri); return; } if (this.state.selectedContact === contact) { if (this.state.chat) { this.setState({chat: false}); } return; } else { this.setState({chat: false}); } let new_value = new_uri; if (contact) { if (this.state.targetUri === contact.uri) { new_value = ''; } } else { contact = null; } if (this.state.targetUri === new_uri) { new_value = ''; } if (new_value === '') { contact = null; } if (new_value.indexOf(' ') === -1) { new_value = new_value.trim().toLowerCase(); } //new_value = new_value.replace(' ',''); //console.log('--- Select new contact', contact? contact.uri : null); //console.log('--- Select new targetUri', new_value); this.props.selectContact(contact); this.setState({targetUri: new_value}); } handleTargetSelect() { if (this.props.connection === null) { this.props._notificationCenter.postSystemNotification("Server unreachable"); return; } let uri = this.state.targetUri.toLowerCase(); if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { let participants; if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(uri)) { participants = this.state.myInvitedParties[uri]; } this.props.startConference(uri, {audio: true, video: true, participants: this.state.participants}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: true}); } } shareContent() { this.props.shareContent(); } showConferenceModal(event) { event.preventDefault(); this.props.showConferenceModalFunc(); } handleChat(event) { event.preventDefault(); let targetUri; if (!this.state.chat && !this.state.selectedContact) { targetUri = this.getTargetUri(this.state.targetUri); this.setState({targetUri: targetUri}); } let uri = this.state.targetUri.trim().toLowerCase(); if (!this.state.chat && !this.selectedContact && uri) { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.props.defaultDomain; } let contact = this.props.newContactFunc(uri, null, {src: 'new chat'}); console.log('Create synthetic contact', contact); this.props.selectContact(contact); this.setState({targetUri: uri, chat: true}); Keyboard.dismiss(); //this.handleTargetChange(targetUri, contact); } this.setState({chat: !this.state.chat}); } handleAudioCall(event) { event.preventDefault(); Keyboard.dismiss(); let uri = this.state.targetUri.trim().toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(uri, {audio: true, video: false}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: false}); } } handleVideoCall(event) { event.preventDefault(); Keyboard.dismiss(); let uri = this.state.targetUri.toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } if (uri.endsWith(`@${config.defaultConferenceDomain}`)) { this.props.startConference(uri, {audio: true, video: true}); } else { this.props.startCall(this.getTargetUri(uri), {audio: true, video: true}); } } handleConferenceCall(targetUri, options={audio: true, video: true, participants: []}) { Keyboard.dismiss(); this.props.startConference(targetUri, {audio: options.audio, video: options.video, participants: options.participants}); this.props.hideConferenceModalFunc(); } get chatButtonDisabled() { let uri = this.state.targetUri.trim(); if (this.state.shareToContacts) { return true; } if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } if (uri.indexOf('@') > -1) { let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; let validEmail = email_reg.test(uri); if (!validEmail) { return true; } } if (this.chatDisabledForUri(uri)) { return true; } return false; } get callButtonDisabled() { let uri = this.state.targetUri.trim(); if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } if (this.state.shareToContacts) { return true; } if (uri.indexOf('@') > -1) { let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; let validEmail = email_reg.test(uri); if (!validEmail) { return true; } } return false; } get videoButtonDisabled() { let uri = this.state.targetUri.trim(); if (!uri || uri.indexOf(' ') > -1 || uri.indexOf('@guest.') > -1 || uri.indexOf('@videoconference') > -1) { return true; } if (uri.indexOf('4444@') > -1) { return true; } if (this.state.shareToContacts) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } return this.callButtonDisabled; } get conferenceButtonDisabled() { let uri = this.state.targetUri.trim(); if (uri.indexOf(' ') > -1) { return true; } if (this.state.shareToContacts) { return true; } let username = uri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { return true; } if (uri.indexOf('@') > -1 && uri.indexOf(config.defaultConferenceDomain) === -1) { return true; } var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; if (event === 'call') { return true; } } return false; } renderNavigationItem(object) { if (!object.item.enabled) { return (null); } let title = object.item.title; let key = object.item.key; let buttonStyle = object.item.selected ? styles.navigationButtonSelected : styles.navigationButton; return (); } render() { let uriClass = styles.portraitUriInputBox; let uriGroupClass = styles.portraitUriButtonGroup; let titleClass = styles.portraitTitle; let uri = this.state.targetUri.toLowerCase(); var uri_parts = uri.split("/"); if (uri_parts.length === 5 && uri_parts[0] === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link let event = uri_parts[3]; uri = uri_parts[4]; if (event === 'conference') { uri = uri.split("@")[0] + '@' + config.defaultConferenceDomain; } } - //console.log('Render ready with chat', this.state.chat); - //this.props.fetchSharedItems(); - const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; if (this.props.isTablet) { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTabletTitle : styles.portraitTabletTitle; } else { titleClass = this.props.orientation === 'landscape' ? styles.landscapeTitle : styles.portraitTitle; } if (this.props.isTablet) { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriButtonGroup : styles.portraitTabletUriButtonGroup; } else { uriGroupClass = this.props.orientation === 'landscape' ? styles.landscapeUriButtonGroup : styles.portraitUriButtonGroup; } if (this.props.isTablet) { uriClass = this.props.orientation === 'landscape' ? styles.landscapeTabletUriInputBox : styles.portraitTabletUriInputBox; } else { uriClass = this.props.orientation === 'landscape' ? styles.landscapeUriInputBox : styles.portraitUriInputBox; } const historyContainer = this.props.orientation === 'landscape' ? styles.historyLandscapeContainer : styles.historyPortraitContainer; const buttonGroupClass = this.props.orientation === 'landscape' ? styles.landscapeButtonGroup : styles.buttonGroup; const borderClass = this.state.chat ? null : styles.historyBorder; let callType = 'Back to call'; if (this.state.call && this.state.call.hasOwnProperty('_participants')) { callType = this.state.selectedContacts.length > 0 ? 'Invite people' : 'Back to conference'; } let navigationMenuData = [ {key: null, title: 'All', enabled: true, selected: false}, {key: 'history', title: 'Calls', enabled: true, selected: this.state.historyCategoryFilter === 'history'}, {key: 'chat', title: 'Chat', enabled: true, selected: this.state.historyCategoryFilter === 'chat'}, {key: 'today', title: 'Today', enabled: this.state.navigationItems['today'], selected: this.state.historyPeriodFilter === 'today'}, {key: 'yesterday', title: 'Yesterday', enabled: this.state.navigationItems['yesterday'], selected: this.state.historyPeriodFilter === 'yesterday'}, {key: 'missed', title: 'Missed', enabled: this.state.missedCalls.length > 0, selected: this.state.historyCategoryFilter === 'missed'}, {key: 'favorite', title: 'Favorites', enabled: this.state.favoriteUris.length > 0, selected: this.state.historyCategoryFilter === 'favorite'}, {key: 'blocked', title: 'Blocked', enabled: this.state.blockedUris.length > 0, selected: this.state.historyCategoryFilter === 'blocked'}, {key: 'conference', title: 'Conference', enabled: Object.keys(this.state.myInvitedParties).length > 0 || this.state.navigationItems['conference'], selected: this.state.historyCategoryFilter === 'conference'}, {key: 'test', title: 'Test', enabled: !this.state.shareToContacts, selected: this.state.historyCategoryFilter === 'test'}, ]; return ( {this.showSearchBar && !this.props.isLandscape ? : null} {this.showButtonsBar ? {this.showSearchBar && this.props.isLandscape ? : null} - {( this.state.call && this.state.call.state == 'established') ? + {this.state.call ? : } : null} { !this.state.selectedContact ? item.key} renderItem={this.renderNavigationItem} /> : null} {this.props.isTablet && 0? : null} ); } } ReadyBox.propTypes = { account : PropTypes.object, password : PropTypes.string.isRequired, config : PropTypes.object.isRequired, startCall : PropTypes.func.isRequired, startConference : PropTypes.func.isRequired, contacts : PropTypes.array, orientation : PropTypes.string, isTablet : PropTypes.bool, isLandscape : PropTypes.bool, refreshHistory : PropTypes.bool, refreshFavorites: PropTypes.bool, saveHistory : PropTypes.func, localHistory : PropTypes.array, myDisplayName : PropTypes.string, myPhoneNumber : PropTypes.string, toggleFavorite : PropTypes.func, myInvitedParties: PropTypes.object, toggleBlocked : PropTypes.func, favoriteUris : PropTypes.array, blockedUris : PropTypes.array, defaultDomain : PropTypes.string, saveContact : PropTypes.func, selectContact : PropTypes.func, lookupContacts : PropTypes.func, call : PropTypes.object, goBackFunc : PropTypes.func, messages : PropTypes.object, sendMessage : PropTypes.func, reSendMessage : PropTypes.func, confirmRead : PropTypes.func, deleteMessage : PropTypes.func, expireMessage : PropTypes.func, getMessages : PropTypes.func, deleteMessages : PropTypes.func, pinMessage : PropTypes.func, unpinMessage : PropTypes.func, sendPublicKey : PropTypes.func, inviteContacts : PropTypes.bool, shareToContacts : PropTypes.bool, selectedContacts: PropTypes.array, updateSelection : PropTypes.func, loadEarlierMessages: PropTypes.func, newContactFunc : PropTypes.func, missedCalls : PropTypes.array, messageZoomFactor: PropTypes.string, isTyping: PropTypes.bool, navigationItems: PropTypes.object, showConferenceModal: PropTypes.bool, showConferenceModalFunc: PropTypes.func, hideConferenceModalFunc: PropTypes.func, shareContent: PropTypes.func, fetchSharedItems: PropTypes.func }; export default ReadyBox; diff --git a/app/utils.js b/app/utils.js index 3581822..133ea6b 100644 --- a/app/utils.js +++ b/app/utils.js @@ -1,286 +1,303 @@ import uuidv4 from 'uuid/v4'; import SillyNames from './SillyNames'; import MaterialColors from './MaterialColors'; import { Clipboard, Dimensions } from 'react-native'; import Contacts from 'react-native-contacts'; import xss from 'xss'; const RNFS = require('react-native-fs'); const logfile = RNFS.DocumentDirectoryPath + '/logs.txt'; function log2file(text) { // append to logfile RNFS.appendFile(logfile, text + '\r\n', 'utf8') .then((success) => { console.log(text); }) .catch((err) => { console.log(err.message); }); } function isAnonymous(uri) { if (uri.indexOf('@guest.') > -1 || uri.indexOf('@anonymous.') > -1) { return true } if (uri.indexOf('@192.168.') > -1) { return true; } if (uri.indexOf('@10.') > -1) { return true; } return false; } function appendLeadingZeroes(n){ if (n <= 9) { return "0" + n; } return n; } function timestampedLog() { let current_datetime = new Date(); let formatted_date = current_datetime.getFullYear() + "-" + appendLeadingZeroes(current_datetime.getMonth() + 1) + "-" + appendLeadingZeroes(current_datetime.getDate()) + " " + appendLeadingZeroes(current_datetime.getHours()) + ":" + appendLeadingZeroes(current_datetime.getMinutes()) + ":" + appendLeadingZeroes(current_datetime.getSeconds()); let message = formatted_date; for (var i = 0; i < arguments.length; i++) { let txt = arguments[i] ? arguments[i].toString() : ''; message = message + ' ' + txt; } log2file(message); //console.log(message); } function generateUniqueId() { const uniqueId = uuidv4().replace(/-/g, '').slice(0, 16); return uniqueId; } function sylkToRenderMessage(sylkMessage, decryptedBody=null, direction='incoming') { //console.log(sylkToRenderMessage, sylkMessage); direction = direction || sylkMessage.direction; /* export interface IMessage { _id: string | number text: string createdAt: Date | number user: User image?: string video?: string audio?: string system?: boolean sent?: boolean received?: boolean pending?: boolean quickReplies?: QuickReplies } */ let system = false; let image; let content = decryptedBody || sylkMessage.content; if (content.indexOf('Welcome!') > -1) { system = true; } if (sylkMessage.contentType === 'text/html') { content = html2text(content); } else if (sylkMessage.contentType === 'text/plain') { content = content; } else if (sylkMessage.contentType.indexOf('image/') > -1) { image = `data:${sylkMessage.contentType};base64,${btoa(content)}` } else { content = 'Unknown message type received ' + sylkMessage.contentType; } let g_id = sylkMessage.id; return { _id: g_id, text: content, image: image, createdAt: sylkMessage.timestamp, received: direction === 'incoming', direction: direction, system: system, user: direction === 'incoming' ? {_id: sylkMessage.sender.uri, name: sylkMessage.sender.toString()} : {} } } function html2text(content) { content = xss(content, { whiteList: [], // empty, means filter out all tags stripIgnoreTag: true, // filter out all HTML not in the whitelist stripIgnoreTagBody: ["script"] // the script tag is a special case, we need // to filter out its content }); return escapeHtml(content); } function normalizeUri(uri, defaultDomain) { let targetUri = uri; let idx = targetUri.indexOf('@'); let username; let domain; if (idx !== -1) { username = targetUri.substring(0, idx); domain = targetUri.substring(idx + 1); } else { username = targetUri; domain = defaultDomain; } username = username.replace(/[<>\s()\[\]\'\"\~\!\%\&\*\{\}\|\\]/g, ''); return `${username}@${domain}`; } function copyToClipboard(text) { Clipboard.setString(text); return true; } function findContact(uri) { return new Promise((resolve, reject) => { //console.log('findContact') Contacts.checkPermission((err, permission) => { if (err) { //log the error console.log(err); return reject(err); } if (permission === 'authorized') { //console.log('HELLO', uri); Contacts.getContactsByEmailAddress(uri, (err, contacts) => { if (err) { console.log('error getting contacts by email') return reject(err); } if (contacts) { return resolve(contacts) } Contacts.getContactsMatchingString(uri, (err2, contacts2) => { if (err2) { console.log('error matching string') return reject(err2); } console.log(contacts); resolve(contacts2) }); }) } else { console.log('not authortised') reject(new Error('Not Authorised')) } }) }) } function generateSillyName() { const adjective = SillyNames.randomAdjective(); const number = Math.floor(Math.random() * 10); const noun1 = SillyNames.randomNoun(); const noun2 = SillyNames.randomNoun(); return adjective + noun1 + noun2 + number; } function generateMaterialColor(text) { return MaterialColors.generateColor(text); } function generateVideoTrack(stream, width = 640, height = 480) { // const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // const analyser = audioCtx.createAnalyser(); // const source = audioCtx.createMediaStreamSource(stream); // source.connect(analyser); // analyser.fftSize = 256; // const bufferLength = analyser.frequencyBinCount; // const dataArray = new Uint8Array(bufferLength); // const canvas = Object.assign(document.createElement('canvas'), {width, height}); // const ctx = canvas.getContext('2d'); // const img = new Image(); // const blinkLogo = new Image(); // img.addEventListener('load', () => { // draw(); // }); // const draw = () => { // if (stream.active) { // const drawVisual = requestAnimationFrame(draw); // } // analyser.getByteFrequencyData(dataArray); // ctx.fillStyle = 'rgb(35, 35, 35)'; // ctx.fillRect(0, 0, width, height); // ctx.filter = 'grayscale(100%) brightness(90%)'; // ctx.drawImage(blinkLogo, (width / 2) - 150, (height / 2) - 150, 300, 300); // ctx.filter = 'none'; // ctx.drawImage(img, (width / 2) - 45 , height / 3, 90, 90); // const barWidth = (width / bufferLength) * 2.5; // let barHeight; // let x = 0; // for(var i = 0; i < bufferLength; i++) { // barHeight = dataArray[i] / 2; // ctx.fillStyle = 'rgb(' + (barHeight + 100) + ', 50, 50)'; // ctx.fillRect(x, 2 * height / 3 - barHeight / 2, barWidth, barHeight); // x += barWidth + 1; // } // }; // img.src = 'assets/images/video-camera-slash.png'; // blinkLogo.src = 'assets/images/blink-white-big.png'; // const canvasStream = canvas.captureStream(); return Object.assign(stream.getVideoTracks()[0], {enabled: true}); } function getWindowHeight() { return Dimensions.get('window').height; } function escapeHtml(text) { var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } +function isPhoneNumber(uri) { + let username = uri; + if (uri.indexOf('@') > -1) { + username = uri.split('@')[0].trim(); + } + return username.match(/^(\+|0)(\d+)$/); +} + +function isEmailAddress(uri) { + uri = uri.trim().toLowerCase(); + let email_reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$/; + return email_reg.test(uri); +} + exports.copyToClipboard = copyToClipboard; exports.normalizeUri = normalizeUri; exports.generateSillyName = generateSillyName; exports.timestampedLog = timestampedLog; exports.appendLeadingZeroes = appendLeadingZeroes; exports.generateUniqueId = generateUniqueId; exports.generateMaterialColor = generateMaterialColor; exports.generateVideoTrack = generateVideoTrack; exports.getWindowHeight = getWindowHeight; exports.findContact = findContact; exports.sylkToRenderMessage = sylkToRenderMessage; exports.isAnonymous = isAnonymous; exports.escapeHtml = escapeHtml; exports.html2text = html2text; +exports.isEmailAddress = isEmailAddress; +exports.isPhoneNumber = isPhoneNumber; + diff --git a/ios/sylk/Info.plist b/ios/sylk/Info.plist index 9d00b55..4845738 100644 --- a/ios/sylk/Info.plist +++ b/ios/sylk/Info.plist @@ -1,134 +1,134 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - sylk + Sylk CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.agprojects.sylk-ios CFBundleURLSchemes sylk CFBundleTypeRole Viewer CFBundleURLName com.agprojects.sylk-ios CFBundleURLSchemes sip CFBundleVersion $(CURRENT_PROJECT_VERSION) FirebaseAppDelegateProxyEnabled FirebaseScreenReportingEnabled ITSAppUsesNonExemptEncryption LSApplicationCategoryType LSApplicationQueriesSchemes message sip readdle-spark airmail ms-outlook googlegmail inbox-gmail ymail superhuman LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads NSCameraUsageDescription Camera is used for video calls NSContactsUsageDescription Contacts permission is needed to call to phone numbers or email addresses found in addressbook NSLocationWhenInUseUsageDescription NSMicrophoneUsageDescription Microphone is used for audio calls NSPhotoLibraryUsageDescription Library is used to send old photos and videos UIAppFonts AntDesign.ttf Entypo.ttf EvilIcons.ttf Feather.ttf FontAwesome.ttf FontAwesome5_Brands.ttf FontAwesome5_Regular.ttf FontAwesome5_Solid.ttf Fontisto.ttf Foundation.ttf Ionicons.ttf MaterialIcons.ttf MaterialCommunityIcons.ttf SimpleLineIcons.ttf Octicons.ttf Zocial.ttf UIBackgroundModes fetch remote-notification voip UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance