diff --git a/README.md b/README.md index 57fb2cd..f455eca 100644 --- a/README.md +++ b/README.md @@ -1,243 +1,276 @@ # Sylk Mobile Sylk Mobile is part of Sylk Suite, a set of real-time communications applications using IETF SIP protocol and WebRTC specifications. Sylk Suite consists of: -* SIP/WebRTC application server -* Mobile push notifications server -* Desktop clients for Windows, Linux and MacOS -* Mobile clients for Apple iOS and Google Android +* Sylk SIP/WebRTC application server +* Sylk mobile push notifications server +* Sylk desktop client for Windows, Linux and MacOS +* Sylk mobile client for Apple iOS and Google Android * Web page for WebRTC enabled browsers -* Mobile client development SDK -* Desktop client development SDK +* Mobile client API development SDK +* Desktop client API development SDK [Home page](https://sylkserver.com) ## License Sylk mobile licensed under GNU General Public License version 3. A copy of the license is available at http://www.fsf.org/licensing/licenses/gpl-3.0.html Copyright 2022 [AG Projects](https://ag-projects.com) ## Availability * [SIP2SIP Web page](https://sip2sip.info) * [Google Play Store](https://play.google.com/store/apps/details?id=com.agprojects.sylk) * [Apple Store](https://apps.apple.com/us/app/id1489960733) * [Beta versions](https://sylkserver.com) * [Source code](https://github.com/AGProjects/sylk-mobile) ## Features * 1-to-1 audio and video calls * Encrypted end-to-end messaging +* Encrypted end-to-end file transfers * Synchronization of multiple devices -* Multiparty conferencing +* Multiparty conferencing for all supported media * Call history entries management * Native address book lookup * Native OS telephony integration * Support for multiple devices in parallel * Support for multiple cameras * Support for landscape and portrait modes * Support for tablets and phones * Interoperable with SIP clients * Receive calls from the web * Support for Self Sovereign Identity (SSI) Messages are encrypted end-to-end using OpenPGP. +File transfer are encrypted end-to-end using OpenPGP whenever possible. + SSI support is based on Hyperledger provided by Indy and Animo SDKs. ## Credits ### Financial support from: * NGI0 PET Fund, a fund established by NLnet with financial support from the European Commission's Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement [No 825310](https://nlnet.nl/project/SylkMobile/) * NGI eSSIF-Lab program, in collaboration with [Bloqzone](https://bloqzone.com) [ADDING SSI TO INTERNET COMMS USING SYLK SUITE](https://www.ngi.eu/funded_solution/essi_ioc_44/) ### People * Adrian Georgescu - Project lead * Saúl Ibarra Corretgé - Inception architect / original idea * Tijmen de Mes - API, Conference, Chat and desktop features * Dan Jenkins - WebRTC and React Native mechanic * Michiel Leenaars - Strategic guidance * Alexander Blom - SSIcomms partnership * Karim Stekelenburg - Animo SDK integration ## Running dependencies * Generic SIP infrastructure -* Sylk Server http://sylkserver.com -* Sylk Push Server http://sylkserver.com + + +## Developing dependencies + * [Janus](https://github.com/meetecho/janus-gateway) Gateway * [Animo SDK for SSI support](https://github.com/animo/aries-mobile-sdk) ## Getting Started ### Building dependencies * Java from http://java.com * NVM from https://heynode.com/tutorial/install-nodejs-locally-nvm/ * Using nvm install Node.js version 12 * Yarn (for package management) curl -o- -L https://yarnpkg.com/install.sh | bash * XCode * Android Studio (Or at least the Android SDK) + export JAVA_HOME="/Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home" + export PATH=$JAVA_HOME/bin:$PATH + export ANDROID_SDK_ROOT=/Users/example/Library/Android/sdk + + On Mac Copy tools.jar to the following location: + sudo cp /Applications/Android\ Studio.app/Contents/jre/jdk/Contents/Home/lib/tools.jar \ + /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib/ + * Gem (for installing gem files) * Fastlane (for deploying to testflight/google play store) * Cocoapods (for handling iOS Pods) - Install RVM: curl -L https://get.rvm.io | bash -s stable - Update ruby: rvm install rvm install ruby - Add to .bash_profile export LANG=en_US.UTF-8 export LANGUAGE=en_US.UTF-8 export LC_ALL=en_US.UTF-8 - sudo gem install cocoapods * watchman (for helping watch files during development) brew install watchman or port install watchman * Hyperledger Indy SDK from https://github.com/hyperledger/indy-sdk + Download libindy libraries from https://repo.sovrin.org/android/libindy/main/ + + https://repo.sovrin.org/android/libindy/main/1.16.0-1/libindy_android_arm64_1.16.0.zip + https://repo.sovrin.org/android/libindy/main/1.16.0-1/libindy_android_arm_1.16.0.zip + https://repo.sovrin.org/android/libindy/main/1.16.0-1/libindy_android_armv7_1.16.0.zip + https://repo.sovrin.org/android/libindy/main/1.16.0-1/libindy_android_x86_1.16.0.zip + https://repo.sovrin.org/android/libindy/main/1.16.0-1/libindy_android_x86_64_1.16.0.zip + + To ./android/app/src/main/jniLibs$ + ### Install Follow the [Getting Started guide](https://facebook.github.io/react-native/docs/getting-started) as much as you can but not everything will be explained. No install docs will be listed here for each tool as they'll change, go and check them out yourself. ### Updating the app Yarn can be a bit of a pain, especially when a git dependency changes. To be sure you're running the lastest code run: ```bash rm -rf node_modules -rm -rf ios/Pods +rm -r android/app/build yarn cache clean yarn -cd ios; pod install; cd .. +rm -rf ios/Pods +rm Podfile.lock +rm -r ~/.cocoapods +cd ios; pod setup; pod install; cd .. ``` +You might want to bring the project back to a clean repo if you're hitting any issues. + +Try it as a dry-run first + +```bash +git clean -d -x --dry-run +``` + +```bash +git clean -d -x -f +``` ### XCODE 12.5 fixes 1. https://infinitbility.com/build-failed-after-update-xcode-12.5 2. https://github.com/foundation/foundation-cli/issues/98 npm rebuild node-sass 3. https://github.com/facebook/create-react-app/issues/4540 brew install watchman or port install watchman ### Running the app Use `react-native run-ios --help` and `react-native run-android --help` to give you all you need to know. You shouldn't ever have to build from Xcode or Android Studio. ### Running on the iOS Simulator Currently we have issues running a build of ios from the cli using `yarn react-native run-ios` so instead, open up xcode and run it there ```bash open ios/sylk.xcworkspace/ ``` ### Running on the Android Simulator or device If you don't have any simulators running, and don't have an android device plugged in (or available to adb) React Native will start up a simulator for you. If you have a device available (doesn't matter if its real or a simulator) this command will output to the device. ```bash yarn react-native run-android ``` +To see the logs of the attached Android device: + +adb logcat '*:W'. + + ### Debugging Install https://reactnative.dev/docs/debugging#react-developer-tools Shake the device and touch Debug. +In XCode enable debugger: + +Product -> Scheme -> Edit -> Run -> Build Configuration -> Debug + ### Running on the iOS Device Currently we have issues running a build of ios from the cli using `yarn react-native run-ios --device` so instead, open up xcode and run it there ### Running on a specific Android Device ```bash yarn react-native run-android --deviceId "DeviceId" ``` > --deviceId [string] builds your app and starts it on a specific device/simulator with the given device id (listed by running "adb devices" on the command line). ### Running without debugging To run the app on your device without tethering it to USB: On Android: ```bash yarn react-native run-android --variant=release ``` On iOS: Select menu Product -> Scheme -> Edit scheme andselect for Run Build Configuration = Release Beware that iOS push tokens are still meant for sandbox unless the app is released through Apple Store. -### Clean the project - -You might want to bring the project back to a clean repo if you're hitting any issues. - -Try it as a dry-run first - -```bash -git clean -d -x --dry-run -``` - -```bash -git clean -d -x -f -``` ### Building the app for deployment We use `fastlane` for building production versions of the app. Fastlane can handle all the metadata around your entry into the relevant App Stores and much much more too. Currently we have two commands - you will need to open Xcode and allow it to sync the deployment key as we allow Xcode to control that rather than do it ourselves. We could add it directly into the project with git-crypt and tell fastlane to use it to make this easier. ```bash fastlane ios beta ``` ```bash fastlane android beta ``` ### Patches We utilise the [patch-package](https://www.npmjs.com/package/patch-package) module in order to patch the `react-native-callkeep` module instead of maintaining a complete fork. See their README on how to make changes to the patch and how those patches get installed automatically within this project on install of npm modules. ### Update sylkrts.js yarn add -D AGProjects/sylkrtc.js ### Firebase notifications reference https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidNotification diff --git a/android/app/build.gradle b/android/app/build.gradle index 46926df..2997315 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,239 +1,245 @@ apply plugin: "com.android.application" +apply plugin: 'kotlin-android' import com.android.build.OutputFile /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets * and bundleReleaseJsAndAssets). * These basically call `react-native bundle` with the correct arguments during the Android build * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the * bundle directly from the development server. Below you can see all the possible configurations * and their defaults. If you decide to add a configuration block, make sure to add it before the * `apply from: "../../node_modules/react-native/react.gradle"` line. * * project.ext.react = [ * // the name of the generated asset file containing your JS bundle * bundleAssetName: "index.android.bundle", * * // the entry file for bundle generation * entryFile: "index.android.js", * * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format * bundleCommand: "ram-bundle", * * // whether to bundle JS and assets in debug mode * bundleInDebug: false, * * // whether to bundle JS and assets in release mode * bundleInRelease: true, * * // whether to bundle JS and assets in another build variant (if configured). * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants * // The configuration property can be in the following formats * // 'bundleIn${productFlavor}${buildType}' * // 'bundleIn${buildType}' * // bundleInFreeDebug: true, * // bundleInPaidRelease: true, * // bundleInBeta: true, * * // whether to disable dev mode in custom build variants (by default only disabled in release) * // for example: to disable dev mode in the staging build type (if configured) * devDisabledInStaging: true, * // The configuration property can be in the following formats * // 'devDisabledIn${productFlavor}${buildType}' * // 'devDisabledIn${buildType}' * * // the root of your project, i.e. where "package.json" lives * root: "../../", * * // where to put the JS bundle asset in debug mode * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", * * // where to put the JS bundle asset in release mode * jsBundleDirRelease: "$buildDir/intermediates/assets/release", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in debug mode * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in release mode * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", * * // by default the gradle tasks are skipped if none of the JS files or assets change; this means * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to * // date; if you have any other folders that you want to ignore for performance reasons (gradle * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ * // for example, you might want to remove it from here. * inputExcludes: ["android/**", "ios/**"], * * // override which node gets called and with what additional arguments * nodeExecutableAndArgs: ["node"], * * // supply additional arguments to the packager * extraPackagerArgs: [] * ] */ project.ext.react = [ entryFile: "index.js", enableHermes: false, // clean and rebuild if changing ] apply from: "../../node_modules/react-native/react.gradle" /** * Set this to true to create two separate APKs instead of one: * - An APK that only works on ARM devices * - An APK that only works on x86 devices * The advantage is the size of the APK is reduced by about 4MB. * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ def enableSeparateBuildPerCPUArchitecture = false /** * Run Proguard to shrink the Java bytecode in release builds. */ def enableProguardInReleaseBuilds = false /** * The preferred build flavor of JavaScriptCore. * * For example, to use the international variant, you can use: * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ def jscFlavor = 'org.webkit:android-jsc:+' /** * Whether to enable the Hermes VM. * * This should be set on project.ext.react and mirrored here. If it is not set * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode * and the benefits of using Hermes will therefore be sharply reduced. */ def enableHermes = project.ext.react.get("enableHermes", false); // Load keystore def keystorePropertiesFile = rootProject.file("keystore.properties"); def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { compileSdkVersion rootProject.ext.compileSdkVersion dexOptions { javaMaxHeapSize "3g" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { applicationId "com.agprojects.sylk" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" missingDimensionStrategy "react-native-camera", "general" - versionCode 226 - versionName "2.9.5" + versionCode 300 + versionName "3.0.0" multiDexEnabled true } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } signingConfigs { debug { - storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' +// storeFile file('debug.keystore') +// storePassword 'android' +// keyAlias 'androiddebugkey' +// keyPassword 'android' + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] } release { storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] } } buildTypes { debug { signingConfig signingConfigs.debug } release { // Caution! In production, you need to generate your own keystore file. // see https://facebook.github.io/react-native/docs/signed-apk-android. signingConfig signingConfigs.release minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } + + configurations { + all { + exclude group: "com.google.android.gms", module: "play-services-measurement-impl" + exclude group: "com.google.android.gms", module: "play-services-measurement" + exclude group: "com.google.android.gms", module: "play-services-measurement-base" + exclude group: "com.google.android.gms", module: "play-services-measurement-api" + exclude group: "com.google.firebase", module: "firebase-iid" + } + } + // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // https://developer.android.com/studio/build/configure-apk-splits.html def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode } } } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) - - implementation 'com.google.firebase:firebase-core:16.0.0' - implementation "com.google.firebase:firebase-messaging:17.3.0" + implementation project(':react-native-image-picker') implementation 'androidx.activity:activity-ktx:1.2.0-alpha08' implementation 'androidx.fragment:fragment:1.3.0-alpha08' - implementation('com.google.firebase:firebase-analytics') { - exclude module: "play-services-ads-identifier" - exclude module: "play-services-measurement" - exclude module: "play-services-measurement-impl" - exclude module: "play-services-measurement-sdk" - } - implementation "com.facebook.react:react-native:+" // From node_modules implementation "androidx.multidex:multidex:2.0.1" - if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") releaseImplementation files(hermesPath + "hermes-release.aar") } else { implementation jscFlavor } implementation 'net.java.dev.jna:jna:5.2.0' } // Run this once to be able to run the application with BUCK // puts all compile dependencies into folder libs for BUCK to use task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' } apply plugin: 'com.google.gms.google-services' // Google Play services Gradle plugin apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9b6bae7..86ad8a6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,150 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle index 179daff..6f07121 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,59 +1,65 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +def REACT_NATIVE_VERSION = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) buildscript { + ext.kotlin_version = '1.6.0' + ext { - buildToolsVersion = "30.0.0" minSdkVersion = 26 compileSdkVersion = 30 targetSdkVersion = 30 supportLibVersion = "30.0.0" - } repositories { google() jcenter() } dependencies { - classpath("com.android.tools.build:gradle:3.4.3") + classpath('com.android.tools.build:gradle:4.2.2') // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.gms:google-services:4.2.0' // Google Services plugin - + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } subprojects { subproject -> afterEvaluate { project -> if (project.hasProperty("android")) { android { compileSdkVersion = 30 - buildToolsVersion = "30.0.0" } } } } allprojects { + configurations.all { + resolutionStrategy { + // Remove this override in 0.66, as a proper fix is included in react-native itself. + force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION + } + } repositories { mavenLocal() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url("$rootDir/../node_modules/react-native/android") } maven { // Android JSC is installed from npm url("$rootDir/../node_modules/jsc-android/dist") } maven { url 'https://repo.sovrin.org/repository/maven-public' } google() jcenter() maven { url 'https://maven.google.com' } maven { url 'https://jitpack.io' } } } diff --git a/android/gradle.properties b/android/gradle.properties index f844f6c..e8cdaf2 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,27 +1,28 @@ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # Docs: http://reactnative.dev/docs/signed-apk-android android.useAndroidX=true android.enableJetifier=true org.gradle.daemon=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e0c4de3..6d7c9ac 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun May 28 22:33:12 ART 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index e524144..abdb3d6 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,7 +1,10 @@ rootProject.name = 'Sylk' include ':react-native-video' project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer') +include ':react-native-image-picker' +project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') + apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' diff --git a/app/app.js b/app/app.js index 9963065..c1a72bd 100644 --- a/app/app.js +++ b/app/app.js @@ -1,9633 +1,10259 @@ -// copyright AG Projects 2020-2021 +// copyright AG Projects 2020-2022 import React, { Component, Fragment } from 'react'; import { Alert, View, SafeAreaView, ImageBackground, AppState, Linking, Platform, StyleSheet, Vibration, PermissionsAndroid} from 'react-native'; import { DeviceEventEmitter, BackHandler } from 'react-native'; import { Provider as PaperProvider, DefaultTheme } from 'react-native-paper'; import { registerGlobals } from 'react-native-webrtc'; import { Router, Route, Link, Switch } from 'react-router-native'; import history from './history'; import Logger from "../Logger"; import autoBind from 'auto-bind'; import { firebase } from '@react-native-firebase/messaging'; import VoipPushNotification from 'react-native-voip-push-notification'; import uuid from 'react-native-uuid'; import { getUniqueId, getBundleId, isTablet, getPhoneNumber} from 'react-native-device-info'; import RNDrawOverlay from 'react-native-draw-overlay'; import PushNotificationIOS from "@react-native-community/push-notification-ios"; import PushNotification , {Importance} from "react-native-push-notification"; import Contacts from 'react-native-contacts'; import BackgroundTimer from 'react-native-background-timer'; import DeepLinking from 'react-native-deep-linking'; import base64 from 'react-native-base64'; import SoundPlayer from 'react-native-sound-player'; import RNSimpleCrypto from "react-native-simple-crypto"; import OpenPGP from "react-native-fast-openpgp"; import ShortcutBadge from 'react-native-shortcut-badge'; import { getAppstoreAppMetadata } from "react-native-appstore-version-checker"; -//import ReceiveSharingIntent from 'react-native-receive-sharing-intent'; +import ReceiveSharingIntent from 'react-native-receive-sharing-intent'; import {Keyboard} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import RNBackgroundDownloader from 'react-native-background-downloader'; import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions'; import cloneDeep from 'lodash/cloneDeep'; registerGlobals(); import * as sylkrtc from 'react-native-sylkrtc'; import InCallManager from 'react-native-incall-manager'; import RNCallKeep, { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep'; - +import RNFetchBlob from "rn-fetch-blob"; import RegisterBox from './components/RegisterBox'; import ReadyBox from './components/ReadyBox'; import Call from './components/Call'; import Conference from './components/Conference'; import FooterBox from './components/FooterBox'; import StatusBox from './components/StatusBox'; import ImportPrivateKeyModal from './components/ImportPrivateKeyModal'; import IncomingCallModal from './components/IncomingCallModal'; import LogsModal from './components/LogsModal'; import NotificationCenter from './components/NotificationCenter'; import LoadingScreen from './components/LoadingScreen'; import NavigationBar from './components/NavigationBar'; import Preview from './components/Preview'; import CallManager from './CallManager'; import SQLite from 'react-native-sqlite-storage'; //SQLite.DEBUG(true); SQLite.enablePromise(true); import xtype from 'xtypejs'; import xss from 'xss'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import momenttz from 'moment-timezone'; import utils from './utils'; import config from './config'; import storage from './storage'; +import fileType from 'react-native-file-type' +import path from 'react-native-path' import { Agent, AutoAcceptCredential, AutoAcceptProof, BasicMessageEventTypes, ConnectionEventTypes, ConnectionInvitationMessage, ConnectionRecord, ConnectionStateChangedEvent, ConsoleLogger, CredentialEventTypes, CredentialRecord, CredentialState, CredentialStateChangedEvent, HttpOutboundTransport, WsOutboundTransport, InitConfig, LogLevel, } from '@aries-framework/core'; import { AgentEventTypes } from "@aries-framework/core/build/agent/Events"; import {agentDependencies} from '@aries-framework/react-native'; var randomString = require('random-string'); const RNFS = require('react-native-fs'); const logfile = RNFS.DocumentDirectoryPath + '/logs.txt'; import styles from './assets/styles/blink/root.scss'; const backgroundImage = require('./assets/images/dark_linen.png'); const logger = new Logger("App"); function checkIosPermissions() { return new Promise(resolve => PushNotificationIOS.checkPermissions(resolve)); } const KeyOptions = { cipher: "aes256", compression: "zlib", hash: "sha512", RSABits: 4096, compressionLevel: 5 } const incomingCallLabel = 'Incoming call...'; const theme = { ...DefaultTheme, dark: true, roundness: 2, colors: { ...DefaultTheme.colors, primary: '#337ab7', // accent: '#f1c40f', }, }; const URL_SCHEMES = [ 'sylk://', ]; const ONE_SECOND_IN_MS = 1000; const VIBRATION_PATTERN = [ 1 * ONE_SECOND_IN_MS, 1 * ONE_SECOND_IN_MS, 4 * ONE_SECOND_IN_MS ]; let bundleId = `${getBundleId()}`; const deviceId = getUniqueId(); const version = '1.0.0'; const MAX_LOG_LINES = 300; if (Platform.OS == 'ios') { bundleId = `${bundleId}.${__DEV__ ? 'dev' : 'prod'}`; //bundleId = 'com.agprojects.sylk-ios.dev'; } const mainStyle = StyleSheet.create({ MainContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', margin: 0 } }); function _parseSQLDate(key, value) { return new Date(value); } (function() { if ( typeof Object.id == "undefined" ) { var id = 0; - Object.id = function(o) { if ( o && typeof o.__uniqueid == "undefined" ) { Object.defineProperty(o, "__uniqueid", { value: ++id, enumerable: false, // This could go either way, depending on your // interpretation of what an "id" is writable: false }); } return o ? o.__uniqueid : null; }; } })(); - class Sylk extends Component { constructor() { super(); autoBind(this) this._loaded = false; let isFocus = Platform.OS === 'ios'; this.startTimestamp = new Date(); this._initialState = { appState: null, autoLogin: true, inFocus: isFocus, accountId: '', password: '', displayName: '', fontScale: 1, email: '', organization: '', account: null, keyStatus: {}, lastSyncId: null, accountVerified: false, registrationState: null, registrationKeepalive: false, incomingCall: null, currentCall: null, connection: null, showScreenSharingModal: false, status: null, targetUri: '', missedTargetUri: '', loading: null, syncConversations: false, localMedia: null, generatedVideoTrack: false, contacts: [], devices: {}, speakerPhoneEnabled: null, orientation : 'portrait', keyboardVisible: false, Height_Layout : '', Width_Layout : '', outgoingCallUUID: null, incomingCallUUID: null, incomingContact: null, keyboardHeight: 0, hardware: '', phoneNumber: '', isTablet: isTablet(), refreshHistory: false, refreshFavorites: false, myPhoneNumber: null, favoriteUris: [], blockedUris: [], missedCalls: [], initialUrl: null, reconnectingCall: false, muted: false, participantsToInvite: [], myInvitedParties: {}, myContacts: {}, defaultDomain: config.defaultDomain, fileSharingUrl: config.fileSharingUrl, + fileTransferUrl: config.fileTransferUrl, declineReason: null, showLogsModal: false, logs: '', proximityEnabled: true, messages: {}, selectedContact: null, callsState: {}, keys: null, showImportPrivateKeyModal: false, privateKey: null, privateKeyImportStatus: '', privateKeyImportSuccess: false, inviteContacts: false, shareToContacts: false, shareContent: [], selectedContacts: [], pinned: false, callContact: null, messageLimit: 100, messageZoomFactor: 1, messageStart: 0, contactsLoaded: false, replicateContacts: {}, updateContactUris: {}, blockedContacts: {}, decryptingMessages: {}, purgeMessages: [], showCallMeMaybeModal: false, enrollment: false, contacts: [], isTyping: false, avatarPhotos: {}, avatarEmails: {}, showConferenceModal: false, keyDifferentOnServer: false, keyExistsOnServer: false, serverPublicKey: null, generatingKey: false, appStoreVersion: null, firstSyncDone: false, keysNotFound: false, showLogo: true, historyFilter: null, showExportPrivateKeyModal: false, showQRCodeScanner: false, navigationItems: {today: false, yesterday: false, conference: false}, ssiRequired: false, ssiAgent: null, ssiRoles: [], myuuid: null, ssiCredentials: null, - ssiConnections: null + ssiConnections: null, + deletedContacts: {}, + isTexting: false, + filteredMessageIds: [] }; utils.timestampedLog('Init app'); this.timeoutIncomingTimer = null; + this.downloadRequests = {}; + this.uploadRequests = {}; + this.pendingNewSQLMessages = []; this.newSyncMessagesCount = 0; this.syncStartTimestamp = null; this.syncRequested = false; this.mustSendPublicKey = false; this.conferenceEndedTimer = null; this.syncTimer = null; this.lastSyncedMessageId = null; this.outgoingMedia = null; this.participantsToInvite = []; this.tokenSent = false; this.mustLogout = false; this.currentRoute = null; this.pushtoken = null; this.pushkittoken = null; this.intercomDtmfTone = null; this.registrationFailureTimer = null; this.startedByPush = false; this.heartbeats = 0; this.sql_contacts_keys = []; this._onFinishedPlayingSubscription = null this._onFinishedLoadingSubscription = null this._onFinishedLoadingFileSubscription = null this._onFinishedLoadingURLSubscription = null this.cancelRingtoneTimer = null; this.sync_pending_items = []; this.signup = {}; this.last_signup = null; this.keyboardDidShowListener = null; this.state = Object.assign({}, this._initialState); this.myParticipants = {}; this.mySyncJournal = {}; this._historyConferenceParticipants = new Map(); // for saving to local history this._terminatedCalls = new Map(); this.__notificationCenter = null; this.redirectTo = null; this.prevPath = null; this.shouldUseHashRouting = false; this.goToReadyTimer = null; this.incoming_sound_ts = null; this.outgoing_sound_ts = null; this.initialChatContact = null; this.mustPlayIncomingSoundAfterSync = false; this.ssiAgent = null; this.pendingSsiUrl = null; - storage.initialize(); this.callKeeper = new CallManager(RNCallKeep, this.showAlertPanel, this.acceptCall, this.rejectCall, this.hangupCall, this.timeoutCall, this.callKeepStartConference, this.startCallFromCallKeeper, this.toggleMute, this.getConnection, this.addHistoryEntry, this.changeRoute, this.respawnConnection, this.isUnmounted ); if (InCallManager.recordPermission !== 'granted') { /* console.log('InCallManager request record permission'); InCallManager.requestRecordPermission() .then((requestedRecordPermissionResult) => { console.log("InCallManager.requestRecordPermission() requestedRecordPermissionResult: ", requestedRecordPermissionResult); }) .catch((err) => { console.log("InCallManager.requestRecordPermission() catch: ", err); }); */ } else { console.log('InCallManager recordPermission', InCallManager.recordPermission); } + storage.initialize(); + // Load camera/mic preferences storage.get('devices').then((devices) => { if (devices) { this.setState({devices: devices}); } }); storage.get('account').then((account) => { if (account) { + console.log('Account is verified'); this.setState({accountVerified: account.verified}); + this.changeRoute('/ready', 'start_up') + this.handleRegistration(account.accountId, account.password); + } else { + this.changeRoute('/login', 'start_up'); } }); storage.get('keys').then((keys) => { if (keys) { const public_key = keys.public.replace(/\r/g,''); const private_key = keys.private.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; this.setState({keys: keys}); console.log("Loaded PGP public key"); } }).catch((err) => { console.log("PGP keys loading error:", err); }); storage.get('ssi').then((ssi) => { if (ssi) { console.log("Loaded SSI settings", ssi); this.setState({ssiRequired: ssi.required}); } else { console.log("Init SSI settings", ssi); storage.set('ssi', {required: false}); this.setState({ssiRequired: false}); } }).catch((err) => { console.log("SSI settings loading error:", err); }); storage.get('myParticipants').then((myParticipants) => { if (myParticipants) { this.myParticipants = myParticipants; //console.log('My participants', this.myParticipants); } }); storage.get('signup').then((signup) => { if (signup) { this.signup = signup; } }); storage.get('last_signup').then((last_signup) => { if (last_signup) { this.last_signup = last_signup; } }); storage.get('mySyncJournal').then((mySyncJournal) => { if (mySyncJournal) { this.mySyncJournal = mySyncJournal; } }); storage.get('lastSyncedMessageId').then((lastSyncedMessageId) => { if (lastSyncedMessageId) { this.lastSyncedMessageId = lastSyncedMessageId; } }); storage.get('proximityEnabled').then((proximityEnabled) => { this.setState({proximityEnabled: proximityEnabled}); }); if (this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } this.loadPeople(); for (let scheme of URL_SCHEMES) { DeepLinking.addScheme(scheme); } - this.sqlTableVersions = {'messages': 8, + this.sqlTableVersions = {'messages': 9, 'contacts': 7, 'keys': 3} this.updateTableQueries = {'messages': {1: [], 2: [{query: 'delete from messages', params: []}], 3: [{query: 'alter table messages add column unix_timestamp INTEGER default 0', params: []}], 4: [{query: 'alter table messages add column account TEXT', params: []}], 5: [{query: 'update messages set account = from_uri where direction = ?' , params: ['outgoing']}, {query: 'update messages set account = to_uri where direction = ?', params: ['incoming']}], 6: [{query: 'alter table messages add column sender TEXT' , params: []}], 7: [{query: 'alter table messages add column image TEXT' , params: []}, {query: 'alter table messages add column local_url TEXT' , params: []}], - 8: [{query: 'alter table messages add column metadata TEXT' , params: []}] + 8: [{query: 'alter table messages add column metadata TEXT' , params: []}], + 9: [{query: 'alter table messages add column state TEXT' , params: []}] }, 'contacts': {2: [{query: 'alter table contacts add column participants TEXT', params: []}], 3: [{query: 'alter table contacts add column direction TEXT', params: []}, {query: 'alter table contacts add column last_call_media TEXT', params: []}, {query: 'alter table contacts add column last_call_duration INTEGER default 0', params: []}, {query: 'alter table contacts add column last_call_id TEXT', params: []}, {query: 'alter table contacts add column conference INTEGER default 0', params: []}], 4: [{query: 'CREATE TABLE contacts2 as SELECT uri, account, name, organization, tags, participants, public_key, timestamp, direction, last_message, last_message_id, unread_messages, last_call_media, last_call_duration, last_call_id, conference from contacts', params: []}, {query: 'CREATE TABLE contacts3 (uri TEXT, account TEXT, name TEXT, organization TEXT, tags TEXT, participants TEXT, public_key TEXT, timestamp INTEGER, direction TEXT, last_message TEXT, last_message_id TEXT, unread_messages TEXT, last_call_media TEXT, last_call_duration INTEGER default 0, last_call_id TEXT, conference INTEGER default 0, PRIMARY KEY (account, uri))', params: []}, {query: 'drop table contacts', params: []}, {query: 'drop table contacts2', params: []}, {query: 'ALTER TABLE contacts3 RENAME TO contacts', params: []} ], 5: [{query: 'alter table contacts add column email TEXT', params: []}], 6: [{query: 'alter table contacts add column photo BLOB', params: []}], 7: [{query: 'alter table contacts add column email TEXT', params: []}] }, 'keys': {2: [{query: 'alter table keys add column last_sync_id TEXT', params: []}], 3: [{query: 'alter table keys add column my_uuid TEXT', params: []}] } }; this.db = null; this.initSQL(); } async requestStoragePermission() { if (Platform.OS !== 'android') { return; } console.log('Request storage permission'); try { const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE; await PermissionsAndroid.request(permission); Promise.resolve(); } catch (error) { Promise.reject(error); } } async requestCameraPermission() { console.log('Request camera permission'); if (Platform.OS === 'ios') { check(PERMISSIONS.IOS.CAMERA).then((result) => { switch (result) { case RESULTS.UNAVAILABLE: console.log('Camera feature is not available (on this device / in this context)'); break; case RESULTS.DENIED: console.log('Camera permission has not been requested / is denied but requestable'); this._notificationCenter.postSystemNotification("Access to camera is denied. Go to Settings -> Sylk to enable access."); break; case RESULTS.LIMITED: console.log('Camera permission is limited: some actions are possible'); break; case RESULTS.GRANTED: console.log('Camera permission is granted'); break; case RESULTS.BLOCKED: this._notificationCenter.postSystemNotification("Access to camera is denied. Go to Settings -> Sylk to enable access."); console.log('Camera permission is denied and not requestable anymore'); break; } }).catch((error) => { }); return true; } if (Platform.OS === 'android') { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.CAMERA, { title: "Sylk camera permission", message: "Sylk needs access to your camera " + "for video calls", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can use the camera"); return true; } else { console.log("Camera permission denied"); return false; } } catch (err) { console.warn(err); return false; } } } async requestMicPermission() { console.log('Request mic permission'); if (Platform.OS === 'ios') { check(PERMISSIONS.IOS.MICROPHONE).then((result) => { switch (result) { case RESULTS.UNAVAILABLE: console.log('Mic feature is not available (on this device / in this context)'); break; case RESULTS.DENIED: console.log('Mic permission has not been requested / is denied but requestable'); this._notificationCenter.postSystemNotification("Access to microphone is denied. Go to Settings -> Sylk to enable access."); break; case RESULTS.LIMITED: console.log('Mic permission is limited: some actions are possible'); break; case RESULTS.GRANTED: console.log('Mic permission is granted'); break; case RESULTS.BLOCKED: this._notificationCenter.postSystemNotification("Access to microphone is denied. Go to Settings -> Sylk to enable access."); console.log('Mic permission is denied and not requestable anymore'); break; } }).catch((error) => { }); return true; } if (Platform.OS === 'android') { try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: "Sylk microphone permission", message: "Sylk needs access to your microphone " + "for audio calls.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can now use the microphone"); return true; } else { console.log("Microphone permission denied"); return false; } } catch (err) { console.warn(err); return false; } } }; useExistingKeys() { var uri = uuid.v4() + '@' + this.state.defaultDomain; console.log('Send public key to', uri); this.sendPublicKey(uri); this.setState({keyDifferentOnServer: false}); } async saveMyKey(keys) { let keyStatus = this.state.keyStatus; keyStatus['existsLocal'] = true; this.setState({keys: {private: keys.private, public: keys.public, showImportPrivateKeyModal: false, keyStatus: keyStatus }}); let myContacts = this.state.myContacts; if (this.state.account) { this.requestSyncConversations(); this.useExistingKeys(); let accountId = this.state.account.id; if (accountId in myContacts) { } else { myContacts[accountId] = this.newContact(accountId); } myContacts[accountId].publicKey = keys.public; this.saveSylkContact(accountId, myContacts[accountId], 'PGP key generated'); } else { console.log('Send 1st public key later'); this.mustSendPublicKey = true; } let current_datetime = new Date(); const unixTime = Math.floor(current_datetime / 1000); const my_uuid = uuid.v4(); let params = [this.state.accountId, keys.private, keys.public, unixTime, my_uuid]; await this.ExecuteQuery("INSERT INTO keys (account, private_key, public_key, timestamp, my_uuid) VALUES (?, ?, ?, ?, ?)", params).then((result) => { console.log('SQL inserted private key'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') > -1) { this.updateKeySql(keys); } else { console.log('Save keys SQL error:', error); } }); } async saveLastSyncId(id, force=false) { if (!force) { if (!this.state.keys || !this.state.keys.private) { console.log('Skip saving last sync id until we have a private key'); return } if (!this.state.firstSyncDone) { console.log('Skip saving last sync id until first sync is done'); return } } let params = [id, this.state.accountId]; await this.ExecuteQuery("update keys set last_sync_id = ? where account = ?", params).then((result) => { utils.timestampedLog('Saved last message sync id', id); this.setState({lastSyncId: id}); }).catch((error) => { console.log('Save last sync id SQL error:', error); }); } async updateKeySql(keys) { let current_datetime = new Date(); const unixTime = Math.floor(current_datetime / 1000); let params = [keys.private, keys.public, unixTime, this.state.accountId]; await this.ExecuteQuery("update keys set private_key = ?, public_key = ?, timestamp = ? where account = ?", params).then((result) => { console.log('SQL updated private key'); }).catch((error) => { console.log('SQL update keys error:', error); }); } async updateMyUUID() { const my_uuid = uuid.v4(); let params = [my_uuid, this.state.accountId]; await this.ExecuteQuery("update keys set my_uuid = ? where account = ?", params).then((result) => { utils.timestampedLog('My device UUID was updated', my_uuid); this.setState({myuuid: my_uuid}); setTimeout(() => { this.initSSIAgent(); }, 100); }).catch((error) => { console.log('SQL update uuid error:', error); }); } loadMyKeys() { utils.timestampedLog('Loading PGP keys...'); let keys = {}; let lastSyncId; let keyStatus = this.state.keyStatus; this.ExecuteQuery("SELECT * FROM keys where account = ?",[this.state.accountId]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); //console.log('SQL has keys'); keys.public = item.public_key; if (item.public_key) { //keyStatus['serverPublicKey'] === item.public_key) { keyStatus['existsLocal'] = true; this.setState({showImportPrivateKeyModal: false}); } else { keyStatus['existsLocal'] = false; } let my_uuid = item.my_uuid; if (!my_uuid) { this.updateMyUUID(); } else { utils.timestampedLog('My device UUID', my_uuid); this.setState({myuuid: my_uuid}); setTimeout(() => { this.initSSIAgent(); }, 100); } keys.private = item.private_key; utils.timestampedLog('Loaded PGP private key for account', this.state.accountId); if (!item.last_sync_id && this.lastSyncedMessageId) { this.setState({keys: keys}); this.saveLastSyncId(this.lastSyncedMessageId); console.log('Migrated last sync id to SQL database'); storage.remove('lastSyncedMessageId'); lastSyncId = this.lastSyncedMessageId; } else { - 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; + lastSyncId = item.last_sync_id + //lastSyncId = '9e011c85-f83a-4b3c-ac49-0ee7b6bd512c' + //lastSyncId = '435471e5-918e-48b5-9a45-1431bd22475f'; + + console.log('Loaded from SQL las sync id', lastSyncId); + this.setState({keys: keys, lastSyncId: lastSyncId}); } if (this.state.registrationState === 'registered') { this.requestSyncConversations(lastSyncId); } } else { //console.log('SQL has no keys'); keyStatus['existsLocal'] = false; if (this.state.account) { this.generateKeysIfNecessary(this.state.account); } else { console.log('Wait for account become active...'); } } this.setState({contactsLoaded: true, keyStatus: keyStatus}); this.getDownloadTasks(); }); } async getDownloadTasks() { let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); - //console.log('Download lost tasks', lostTasks); + if (lostTasks.length > 0) { + console.log('Download lost tasks', lostTasks); + } + for (let task of lostTasks) { console.log(`Download task ${task.id} was found:`, task.url); - task.progress((percent) => { - console.log(task.url, `Downloaded: ${percent * 100}%`); - }).done(() => { - this.updateFileDownload(id, task.url, task.destination); - }).error((error) => { - console.log(task.url, 'download error:', error); - }); + if (task.url && task.destination) { + task.progress((percent) => { + console.log(task.url, `Downloaded: ${percent * 100}%`); + }).done(() => { + this.saveDownloadTask(id, task.url, task.destination); + }).error((error) => { + console.log(task.url, 'download error:', error); + }); + } } } async generateKeys() { const Options = { comment: 'Sylk key', email: this.state.accountId, name: this.state.displayName || this.state.accountId, keyOptions: KeyOptions } utils.timestampedLog('Generating key pair with options', Options); this.setState({loading: 'Generating private key...', generatingKey: true}); await OpenPGP.generate(Options).then((keys) => { const public_key = keys.publicKey.replace(/\r/g, '').trim(); const private_key = keys.privateKey.replace(/\r/g, '').trim(); keys.public = public_key; keys.private = private_key; utils.timestampedLog("PGP keypair generated"); this.setState({loading: null, generatingKey: false}); this.setState({showImportPrivateKeyModal: false}); this.saveMyKey(keys); this.showCallMeModal(); }).catch((error) => { console.log("PGP keys generation error:", error); }); } resetStorage() { return; console.log('Reset storage'); this.ExecuteQuery('delete from contacts'); this.ExecuteQuery('delete from messages'); this.saveLastSyncId(null); } loadSylkContacts() { console.log('Loading contacts...') let myContacts = {}; let blockedUris = []; let favoriteUris = []; let missedCalls = []; let myInvitedParties = {}; let localTime; let email; let contact; let timestamp; this.loadAddressBook(); if (this.state.accountId in this.signup) { email = this.signup[this.state.accountId]; this.setState({email: email}); } if (!this.last_signup) { storage.set('last_signup', this.state.accountId); if (this.state.accountId in this.signup) { } else { this.signup[this.state.accountId] = ''; storage.set('signup', this.signup); } } this.setState({defaultDomain: this.state.accountId.split('@')[1]}); this.ExecuteQuery("SELECT * FROM contacts where account = ? order by timestamp desc",[this.state.accountId]).then((results) => { let rows = results.rows; let idx; let formatted_date; let updated; //console.log(rows.length, 'SQL rows'); if (rows.length > 0) { for (let i = 0; i < rows.length; i++) { var item = rows.item(i); updated = null; if (!item.uri) { continue; } contact = this.newContact(item.uri, item.name, {src: 'init'}); if (!contact) { continue; } this.sql_contacts_keys.push(item.uri); timestamp = new Date(item.timestamp * 1000); if (timestamp > new Date()) { timestamp = new Date(); updated = 'timestamp'; } myContacts[item.uri] = contact; myContacts[item.uri].organization = item.organization; myContacts[item.uri].email = item.email; myContacts[item.uri].photo = item.photo; myContacts[item.uri].publicKey = item.public_key; myContacts[item.uri].direction = item.direction; myContacts[item.uri].tags = item.tags ? item.tags.split(',') : []; myContacts[item.uri].participants = item.participants ? item.participants.split(',') : []; myContacts[item.uri].unread = item.unread_messages ? item.unread_messages.split(',') : []; myContacts[item.uri].lastMessageId = item.last_message_id === '' ? null : item.last_message_id; myContacts[item.uri].lastMessage = item.last_message === '' ? null : item.last_message; myContacts[item.uri].timestamp = timestamp; myContacts[item.uri].lastCallId = item.last_call_id; myContacts[item.uri].lastCallMedia = item.last_call_media ? item.last_call_media.split(',') : []; myContacts[item.uri].lastCallDuration = item.last_call_duration; let ab_contacts = this.lookupContacts(item.uri); if (ab_contacts.length > 0) { if (!myContacts[item.uri].name || myContacts[item.uri].name === '') { console.log('Update display name', myContacts[item.uri].name, 'of', item.uri, 'to', ab_contacts[0].name); myContacts[item.uri].name = ab_contacts[0].name; updated = 'name'; } myContacts[item.uri].label = ab_contacts[0].label; if (myContacts[item.uri].tags.indexOf('contact') === -1) { myContacts[item.uri].tags.push('contact'); updated = 'tags'; } } if (!myContacts[item.uri].photo) { var name_idx = myContacts[item.uri].name.trim().toLowerCase(); if (name_idx in this.state.avatarPhotos) { myContacts[item.uri].photo = this.state.avatarPhotos[name_idx]; updated = 'photo'; } } if (!myContacts[item.uri].email) { var name_idx = myContacts[item.uri].name.trim().toLowerCase(); if (name_idx in this.state.avatarEmails) { myContacts[item.uri].email = this.state.avatarEmails[name_idx]; updated = 'email'; } } if (myContacts[item.uri].tags.indexOf('missed') > -1) { missedCalls.push(item.last_call_id); if (myContacts[item.uri].unread.indexOf(item.last_call_id) === -1) { myContacts[item.uri].unread.push(item.last_call_id); } } else { idx = myContacts[item.uri].unread.indexOf(item.last_call_id); if (idx > -1) { myContacts[item.uri].unread.splice(idx, 1); } } if (item.uri === this.state.accountId) { this.setState({displayName: item.name, organization: item.organization}); if (email && !item.email) { item.email = email; } else { this.setState({email: item.email}); } } formatted_date = myContacts[item.uri].timestamp.getFullYear() + "-" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getMonth() + 1) + "-" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getDate()) + " " + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getHours()) + ":" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getMinutes()) + ":" + utils.appendLeadingZeroes(myContacts[item.uri].timestamp.getSeconds()); //console.log('Loaded contact', formatted_date, item.uri, item.name); if(item.participants) { myInvitedParties[item.uri.split('@')[0]] = myContacts[item.uri].participants; } if (myContacts[item.uri].tags.indexOf('blocked') > -1) { blockedUris.push(item.uri); } if (myContacts[item.uri].tags.indexOf('favorite') > -1) { favoriteUris.push(item.uri); } if (updated) { this.saveSylkContact(item.uri, myContacts[item.uri], 'update contact at init because of ' + updated); } //console.log('Load contact', item.uri, '-', item.name); } storage.get('cachedHistory').then((history) => { if (history) { //this.cachedHistory = history; history.forEach((item) => { //console.log(item); if (item.remoteParty in myContacts) { } else { myContacts[item.remoteParty] = this.newContact(item.remoteParty); } if (item.timezone && item.timezone !== undefined) { localTime = momenttz.tz(item.startTime, item.timezone).toDate(); if (localTime > myContacts[item.remoteParty].timestamp) { myContacts[item.remoteParty].timestamp = localTime; } } myContacts[item.remoteParty].name = item.displayName; myContacts[item.remoteParty].direction = item.direction === 'received' ? 'incoming' : 'outgoing'; myContacts[item.remoteParty].lastCallId = item.sessionId; myContacts[item.remoteParty].lastCallDuration = item.duration; myContacts[item.remoteParty].lastCallMedia = item.media; myContacts[item.remoteParty].conference = item.conference; myContacts[item.remoteParty].tags.push('history'); this.saveSylkContact(item.remoteParty, this.state.myContacts[item.remoteParty], 'init'); }); console.log('Migrated', history.length, 'server history entries'); storage.remove('cachedHistory'); } }); storage.get('history').then((history) => { if (history) { console.log('Loaded', history.length, 'local history entries'); history.forEach((item) => { if (item.remoteParty in myContacts) { } else { myContacts[item.remoteParty] = this.newContact(item.remoteParty); } if (item.timezone && item.timezone !== undefined) { localTime = momenttz.tz(item.startTime, item.timezone).toDate(); if (localTime > myContacts[item.remoteParty].timestamp) { myContacts[item.remoteParty].timestamp = localTime; } } myContacts[item.remoteParty].name = item.displayName; myContacts[item.remoteParty].direction = item.direction === 'received' ? 'incoming' : 'outgoing'; myContacts[item.remoteParty].lastCallId = item.sessionId; myContacts[item.remoteParty].lastCallDuration = item.duration; myContacts[item.remoteParty].lastCallMedia = item.media; myContacts[item.remoteParty].conference = item.conference; myContacts[item.remoteParty].tags.push('history'); this.saveSylkContact(item.remoteParty, this.state.myContacts[item.remoteParty], 'init'); }); console.log('Migrated', history.length, 'local history entries'); storage.remove('history'); } }); this.updateTotalUread(myContacts); console.log('Loaded', rows.length, 'contacts for account', this.state.accountId); this.setState({myContacts: myContacts, missedCalls: missedCalls, favoriteUris: favoriteUris, myInvitedParties: myInvitedParties, blockedUris: blockedUris}); } else { if (Object.keys(this.state.myContacts).length > 0) { Object.keys(this.state.myContacts).forEach((key) => { this.saveSylkContact(key, this.state.myContacts[key], 'init'); }); storage.set('contactStorage', 'sql'); storage.remove('myContacts'); } } this.refreshNavigationItems(); setTimeout(() => { if (this.initialChatContact) { console.log('Starting chat with', this.initialChatContact); if (this.initialChatContact in this.state.myContacts) { this.selectContact(this.state.myContacts[this.initialChatContact]); } else { this.initialChatContact = null; } } }, 100); setTimeout(() => { //this.getMessages(); }, 500); this.loadMyKeys(); }); } addTestContacts() { let myContacts = this.state.myContacts; let test_numbers = [ {uri: '4444@sylk.link', name: 'Test microphone'}, {uri: '3333@sylk.link', name: 'Test video'} ]; test_numbers.forEach((item) => { if (Object.keys(myContacts).indexOf(item.uri) === -1) { myContacts[item.uri] = this.newContact(item.uri, item.name, {src: 'init'}); myContacts[item.uri].tags.push('test'); this.saveSylkContact(item.uri, myContacts[item.uri], 'init uri'); } else { if (myContacts[item.uri].tags.indexOf('test') === -1) { myContacts[item.uri].tags.push('test'); this.saveSylkContact(item.uri, myContacts[item.uri], 'init tags'); } if (!myContacts[item.uri].name) { myContacts[item.uri].name = item.name; this.saveSylkContact(item.uri, myContacts[item.uri], 'init name'); } } }); } loadPeople() { let myContacts = {}; let blockedUris = []; let favoriteUris = []; let displayName = null; storage.get('contactStorage').then((contactStorage) => { if (contactStorage !== 'sql') { storage.get('myContacts').then((myContacts) => { let myContactsObjects = {}; if (myContacts) { Object.keys(myContacts).forEach((key) => { if (!Array.isArray(myContacts[key]['unread'])) { myContacts[key]['unread'] = []; } if(typeof(myContacts[key]) == 'string') { console.log('Convert display name object'); myContactsObjects[key] = {'name': myContacts[key]} } else { myContactsObjects[key] = myContacts[key]; } }); myContacts = myContactsObjects; } else { myContacts = {}; } this.setState({myContacts: myContacts}); storage.get('favoriteUris').then((favoriteUris) => { favoriteUris = favoriteUris.filter(item => item !== null); //console.log('My favorites:', favoriteUris); this.setState({favoriteUris: favoriteUris}); storage.remove('favoriteUris'); }).catch((error) => { //console.log('get favoriteUris error:', error); let uris = Object.keys(myContacts); uris.forEach((uri) => { if (myContacts[uri].favorite) { favoriteUris.push(uri); } }); this.setState({favoriteUris: favoriteUris}); }); storage.get('blockedUris').then((blockedUris) => { blockedUris = blockedUris.filter(item => item !== null); this.setState({blockedUris: blockedUris}); storage.remove('blockedUris'); }).catch((error) => { //console.log('get blockedUris error:', error); let uris = Object.keys(myContacts); uris.forEach((uri) => { if (myContacts[uri].blocked) { blockedUris.push(uri); } }); this.setState({blockedUris: blockedUris}); }); }).catch((error) => { console.log('get myContacts error:', error); }); } }); } async initSQL() { const database_name = "sylk.db"; const database_version = "1.0"; const database_displayname = "Sylk Database"; const database_size = 200000; await SQLite.openDatabase(database_name, database_version, database_displayname, database_size).then((DB) => { this.db = DB; console.log('SQL database', database_name, 'opened'); this.resetStorage(); //this.dropTables(); this.createTables(); }).catch((error) => { console.log('SQL database error:', error); }); } dropTables() { console.log('Drop SQL tables...') this.ExecuteQuery("DROP TABLE if exists 'chat_uris';"); this.ExecuteQuery("DROP TABLE if exists 'recipients';"); this.ExecuteQuery("DROP TABLE 'messages';"); this.ExecuteQuery("DROP TABLE 'versions';"); } createTables() { //console.log('Create SQL tables...') let create_versions_table = "CREATE TABLE IF NOT EXISTS 'versions' ( \ 'id' INTEGER PRIMARY KEY AUTOINCREMENT, \ 'table' TEXT UNIQUE, \ 'version' INTEGER NOT NULL );\ "; this.ExecuteQuery(create_versions_table).then((success) => { //console.log('SQL version table created'); }).catch((error) => { console.log(create_versions_table); console.log('SQL version table creation error:', error); }); let create_table_messages = "CREATE TABLE IF NOT EXISTS 'messages' ( \ 'msg_id' TEXT, \ 'timestamp' TEXT, \ 'account' TEXT, \ 'unix_timestamp' INTEGER default 0, \ 'sender' TEXT, \ 'content' BLOB, \ 'content_type' TEXT, \ 'metadata' TEXT, \ 'from_uri' TEXT, \ 'to_uri' TEXT, \ 'sent' INTEGER, \ 'sent_timestamp' TEXT, \ 'received' INTEGER, \ 'received_timestamp' TEXT, \ 'expire_interval' INTEGER, \ 'deleted' INTEGER, \ 'pinned' INTEGER, \ 'pending' INTEGER, \ 'system' INTEGER, \ 'url' TEXT, \ 'local_url' TEXT, \ 'image' TEXT, \ 'encrypted' INTEGER default 0, \ 'direction' TEXT, \ + 'state' TEXT, \ PRIMARY KEY (account, msg_id)) \ "; this.ExecuteQuery(create_table_messages).then((success) => { //console.log('SQL messages table OK'); }).catch((error) => { console.log(create_table_messages); console.log('SQL messages table creation error:', error); }); let create_table_contacts = "CREATE TABLE IF NOT EXISTS 'contacts' ( \ 'uri' TEXT, \ 'account' TEXT, \ 'name' TEXT, \ 'organization' TEXT, \ 'tags' TEXT, \ 'photo' BLOB, \ 'email' TEXT, \ 'participants' TEXT, \ 'public_key' TEXT, \ 'timestamp' INTEGER, \ 'direction' TEXT, \ 'last_message' TEXT, \ 'last_message_id' TEXT, \ 'unread_messages' TEXT, \ 'last_call_media' TEXT, \ 'last_call_duration' INTEGER default 0, \ 'last_call_id' TEXT, \ 'conference' INTEGER default 0, \ PRIMARY KEY (account, uri)) \ "; this.ExecuteQuery(create_table_contacts).then((success) => { //console.log('SQL contacts table OK'); }).catch((error) => { console.log(create_table_contacts); console.log('SQL messages table creation error:', error); }); let create_table_keys = "CREATE TABLE IF NOT EXISTS 'keys' ( \ 'account' TEXT PRIMARY KEY, \ 'private_key' TEXT, \ 'checksum' TEXT, \ 'public_key' TEXT, \ 'last_sync_id' TEXT, \ 'timestamp' INTEGER, \ 'my_uuid' TEXT) \ "; this.ExecuteQuery(create_table_keys).then((success) => { //console.log('SQL keys table OK'); }).catch((error) => { console.log(create_table_keys); console.log('SQL keys table creation error:', error); }); this.upgradeSQLTables(); } upgradeSQLTables() { //console.log('Upgrade SQL tables') let query; let update_queries; let update_sub_queries; let version_numbers; /* this.ExecuteQuery("ALTER TABLE 'messages' add column received_timestamp TEXT after received"); this.ExecuteQuery("ALTER TABLE 'messages' add column sent_timestamp TEXT after sent"); */ //query = "update versions set version = \"4\" where \"table\" = 'messages'"; // this.ExecuteQuery(query); query = "SELECT * FROM versions"; let currentVersions = {}; this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; for (let i = 0; i < rows.length; i++) { var item = rows.item(i); currentVersions[item.table] = item.version; //console.log('Table', item.table, 'version', item.version); } for (const [key, value] of Object.entries(this.sqlTableVersions)) { if (currentVersions[key] == null) { query = "INSERT INTO versions ('table', 'version') values ('" + key + "', '" + this.sqlTableVersions[key] + "')"; //console.log(query); this.ExecuteQuery(query); } else { //console.log('Table', key, 'has version', value); if (this.sqlTableVersions[key] > currentVersions[key]) { console.log('Table', key, 'must have version', value, 'and it has', currentVersions[key]); update_queries = this.updateTableQueries[key]; version_numbers = Object.keys(update_queries); version_numbers.sort(function(a, b){return a-b}); version_numbers.forEach((version) => { if (version <= currentVersions[key]) { return; } update_sub_queries = update_queries[version]; update_sub_queries.forEach((query_objects) => { console.log('Run query for table', key, 'version', version, ':', query_objects.query); this.ExecuteQuery(query_objects.query, query_objects.params); }); }); query = "update versions set version = " + this.sqlTableVersions[key] + " where \"table\" = '" + key + "';"; //console.log(query); this.ExecuteQuery(query); } else { //console.log('No upgrade required for table', key); } } } }).catch((error) => { console.log('SQL error:', error); }); } /* * Execute sql queries * * @param sql * @param params * * @returns {resolve} results */ ExecuteQuery = (sql, params = []) => new Promise((resolve, reject) => { //console.log('-- Execute SQL query:', sql, params); //console.log('-- Execute SQL query:', sql); if (!sql) { return; } this.db.transaction((trans) => { trans.executeSql(sql, params, (trans, results) => { resolve(results); }, (error) => { reject(error); }); }); }); async requestReadContactsPermission() { console.log('Request contacts permission...'); try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.READ_CONTACTS, { title: 'Sylk contacts', message: 'Sylk will ask for permission to read your contacts', buttonPositive: "Next" } ) if (granted === PermissionsAndroid.RESULTS.GRANTED) { console.log("You can now read your contacts") this.getABContacts(); } else { console.log("Read contacts permission denied") } } catch (err) { console.warn(err) } } async loadAddressBook() { console.log('Load system address book'); Contacts.checkPermission((err, permission) => { //console.log('Current contacts permissions is', permission); if (err) throw err; // Contacts.PERMISSION_AUTHORIZED || Contacts.PERMISSION_UNDEFINED || Contacts.PERMISSION_DENIED if (permission === 'authorized') { this.getABContacts(); return; } if (Platform.OS === 'android') { this.requestReadContactsPermission(); } else { Contacts.requestPermission((err, permission) => { }); } }) } getABContacts() { Contacts.getAll((err, contacts) => { if (err) throw err; // contacts returned in Array let contact_cards = []; let name; let photo; let avatarPhotos = {}; let avatarEmails = {}; let seen_uris = new Map(); var arrayLength = contacts.length; for (var i = 0; i < arrayLength; i++) { photo = null; contact = contacts[i]; if (contact['givenName'] && contact['familyName']) { name = contact['givenName'] + ' ' + contact['familyName']; } else if (contact['givenName']) { name = contact['givenName']; } else if (contact['familyName']) { name = contact['familyName']; } else if (contact['company']) { name = contact['company']; } else { continue; } if (contact.hasThumbnail) { photo = contact.thumbnailPath; } else { photo = null; } //console.log(name); contact['phoneNumbers'].forEach(function (number, index) { let number_stripped = number['number'].replace(/\s|\-|\(|\)/g, ''); if (number_stripped) { if (!seen_uris.has(number_stripped)) { //console.log(' ----> ', number['label'], number_stripped); var contact_card = {id: uuid.v4(), name: name.trim(), uri: number_stripped, type: 'contact', photo: photo, label: number['label'], tags: ['contact']}; if (photo) { var name_idx = name.trim().toLowerCase(); avatarPhotos[name_idx] = photo; } contact_cards.push(contact_card); //console.log('Added AB contact', name, number_stripped); seen_uris.set(number_stripped, true); } } }); contact['emailAddresses'].forEach(function (email, index) { let email_stripped = email['email'].replace(/\s|\(|\)/g, ''); if (!seen_uris.has(email_stripped)) { //console.log(name, email['label'], email_stripped); var contact_card = {id: uuid.v4(), name: name.trim(), uri: email_stripped, type: 'contact', photo: photo, label: email['label'], tags: ['contact'] }; var name_idx = name.trim().toLowerCase(); if (photo) { avatarPhotos[name_idx] = photo; } if (name_idx in avatarEmails) { } else { avatarEmails[name_idx] = email_stripped; } contact_cards.push(contact_card); seen_uris.set(email_stripped, true); } }); } this.setState({contacts: contact_cards, avatarPhotos: avatarPhotos, avatarEmails: avatarEmails}); console.log('Loaded', contact_cards.length, 'addressbook entries'); }) } get _notificationCenter() { // getter to lazy-load the NotificationCenter ref if (!this.__notificationCenter) { this.__notificationCenter = this.refs.notificationCenter; } return this.__notificationCenter; } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } _detectOrientation() { //console.log('_detectOrientation', this.state.Width_Layout, this.state.Height_Layout); let H = this.state.Height_Layout + this.state.keyboardHeight; if(this.state.Width_Layout > H && this.state.orientation !== 'landscape') { this.setState({orientation: 'landscape'}); } else { this.setState({orientation: 'portrait'}); } } changeRoute(route, reason) { + console.log('Route', route, reason); utils.timestampedLog('Change route', this.currentRoute, '->', route, 'with reason:', reason); let messages = this.state.messages; if (this.currentRoute === route) { if (route === '/ready') { if (this.state.selectedContact) { if (this.state.callContact) { if (this.state.callContact.uri !== this.state.selectedContact.uri && this.state.selectedContact.uri in messages) { delete messages[this.state.selectedContact.uri]; } } else { if (this.state.selectedContact.uri in messages) { delete messages[this.state.selectedContact.uri]; } } this.setState({ messages: messages, selectedContact: null, targetUri: '' }); } else { this.setState({ messages: {}, messageZoomFactor: 1 }); } } return; } else { if (route === '/ready' && this.state.selectedContact && Object.keys(this.state.messages).indexOf(this.state.selectedContact.uri) === -1) { this.getMessages(this.state.selectedContact.uri); } } if (route === '/conference') { this.backToForeground(); this.setState({inviteContacts: false}); } if (route === '/call') { this.backToForeground(); } if (route === '/ready' && reason !== 'back to home') { Vibration.cancel(); if (reason === 'conference_really_ended' && this.callKeeper.countCalls) { utils.timestampedLog('Change route cancelled because we still have calls'); return; } if (this.state.currentCall && reason === 'outgoing_connection_failed' && this.state.currentCall.direction === 'outgoing') { let target_uri = this.state.currentCall.remoteIdentity.uri.toLowerCase(); let options = {audio: true, video: true, participants: []} let streams = this.state.currentCall.getLocalStreams(); if (streams.length > 0) { let tracks = streams[0].getVideoTracks(); let mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; if (mediaType === 'audio') { options.video = false; } } this.setState({reconnectingCall: true}); console.log('Reconnecting call to', target_uri, 'with options', options); setTimeout(() => { if (target_uri.indexOf('@videoconference') > -1) { this.callKeepStartConference(target_uri, options); } else { this.callKeepStartCall(target_uri, options); } }, 5000); this.setState({ outgoingCallUUID: null, currentCall: null, selectedContacts: [], reconnectingCall: true, muted: false }); } else { if (this.state.callContact && this.state.callContact.uri in messages) { delete messages[this.state.callContact.uri]; } this.setState({ outgoingCallUUID: null, currentCall: null, callContact: null, messages: {}, selectedContact: null, inviteContacts: false, shareToContacts: false, selectedContacts: [], incomingCall: (reason === 'accept_new_call' || reason === 'user_hangup_call') ? this.state.incomingCall: null, reconnectingCall: false, muted: false }); } if (this.currentRoute === '/call' || this.currentRoute === '/conference') { if (reason !== 'user_hangup_call') { this.stopRingback(); InCallManager.stop(); } this.closeLocalMedia(); if (reason === 'accept_new_call') { if (this.state.incomingCall) { // then answer the new call if any let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } else if (reason === 'escalate_to_conference') { let conf_uri = []; conf_uri.push(this.state.accountId.split('@')[0]); this.participantsToInvite.forEach((p) => { conf_uri.push(p.split('@')[0]); }); conf_uri.sort(); let uri = conf_uri.toString().toLowerCase().replace(/,/g,'-') + '@' + config.defaultConferenceDomain; const options = {audio: this.outgoingMedia ? this.outgoingMedia.audio: true, video: this.outgoingMedia ? this.outgoingMedia.video: true, participants: this.participantsToInvite, skipHistory: true} this.participantsToInvite = []; this.callKeepStartConference(uri, options); } else { if (this.state.account && this._loaded) { setTimeout(() => { this.updateServerHistory('/ready') }, 1500); } } } if (reason === 'registered') { setTimeout(() => { this.updateServerHistory(reason) }, 1500); } if (reason === 'no_more_calls') { this.updateServerHistory(reason); this.updateLoading(null, 'incoming_call'); this.setState({incomingCallUUID: null}); } if (reason === 'start_up') { - storage.get('account').then((account) => { - if (account) { - this.handleRegistration(account.accountId, account.password); - } else { - this.changeRoute('/login', 'start up'); - } - }); this.fetchSharedItems(); } } this.currentRoute = route; history.push(route); } componentWillUnmount() { utils.timestampedLog('App will unmount'); AppState.removeEventListener('change', this._handleAppStateChange); this._onFinishedPlayingSubscription.remove(); this._onFinishedLoadingSubscription.remove(); this._onFinishedLoadingURLSubscription.remove(); this._onFinishedLoadingFileSubscription.remove(); this.callKeeper.destroy(); this.closeConnection(); this._loaded = false; } get unmounted() { return !this._loaded; } isUnmounted() { return this.unmounted; } backPressed() { console.log('Back button pressed in route', this.currentRoute); if (this.state.incomingCallUUID) { this.hideInternalAlertPanel('backPressed'); return; } if (this.state.showQRCodeScanner) { this.toggleQRCodeScanner(); return; } if (this.currentRoute === '/call' || this.currentRoute === '/conference') { this.goBackToHome(); /* let call = this.state.currentCall || this.state.incomingCall; if (call && call.id) { this.hangupCall(call.id, 'user_hangup_call'); } */ } else if (this.currentRoute === '/ready') { if (this.state.selectedContact) { this.goBackToHome(); } else if (this.state.historyFilter) { this.filterHistory(null); } else { BackHandler.exitApp(); } } return true; } async componentDidMount() { utils.timestampedLog('App did mount'); //this.requestStoragePermission(); DeviceInfo.getFontScale().then((fontScale) => { this.setState({fontScale: fontScale}); }); this.keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', this._keyboardDidShow ); this.keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', this._keyboardDidHide ); BackHandler.addEventListener('hardwareBackPress', this.backPressed); // Start a timer that runs once after X milliseconds BackgroundTimer.runBackgroundTimer(() => { // this will be executed once after 10 seconds // even when app is the the background this.heartbeat(); }, 5000); try { await RNCallKeep.supportConnectionService (); utils.timestampedLog('Connection service is enabled'); } catch(err) { utils.timestampedLog(err); } try { await RNCallKeep.hasPhoneAccount(); utils.timestampedLog('Phone account is enabled'); } catch(err) { utils.timestampedLog(err); } if (Platform.OS === 'android') { RNDrawOverlay.askForDispalayOverOtherAppsPermission() .then(res => { //utils.timestampedLog("Display over other apps was granted"); // res will be true if permission was granted }) .catch(e => { utils.timestampedLog("Display over other apps was declined"); // permission was declined }) } // prime the ref //logger.debug('NotificationCenter ref: %o', this._notificationCenter); this._boundOnPushkitRegistered = this._onPushkitRegistered.bind(this); this._boundOnPushRegistered = this._onPushRegistered.bind(this); this._detectOrientation(); getPhoneNumber().then(phoneNumber => { this.setState({myPhoneNumber: phoneNumber}); }); this.listenforPushNotifications(); this.listenforSoundNotifications(); this._loaded = true; this.checkVersion(); } _keyboardDidShow(e) { this.setState({keyboardVisible: true, keyboardHeight: e.endCoordinates.height}); } _keyboardDidHide() { this.setState({keyboardVisible: false, keyboardHeight: 0}); } checkVersion() { if (Platform.OS === 'android') { getAppstoreAppMetadata("com.agprojects.sylk") //put any apps packageId here .then(metadata => { console.log("Sylk app version on playstore", metadata.version, "published on", metadata.currentVersionReleaseDate ); this.setState({appStoreVersion: metadata}); }) .catch(err => { console.log("error occurred", err); }); return; } else { getAppstoreAppMetadata("1489960733") //put any apps id here .then(appVersion => { console.log("Sylk app version on appstore", appVersion.version, "published on", appVersion.currentVersionReleaseDate); this.setState({appStoreVersion: appVersion}); }) .catch(err => { console.log("Error fetching app store version occurred", err); }); } } listenforSoundNotifications() { // Subscribe to event(s) you want when component mounted this._onFinishedPlayingSubscription = SoundPlayer.addEventListener('FinishedPlaying', ({ success }) => { //console.log('finished playing', success) }) this._onFinishedLoadingSubscription = SoundPlayer.addEventListener('FinishedLoading', ({ success }) => { //console.log('finished loading', success) }) this._onFinishedLoadingFileSubscription = SoundPlayer.addEventListener('FinishedLoadingFile', ({ success, name, type }) => { //console.log('finished loading file', success, name, type) }) this._onFinishedLoadingURLSubscription = SoundPlayer.addEventListener('FinishedLoadingURL', ({ success, url }) => { //console.log('finished loading url', success, url) }) } handleFirebasePushInForeground(parent) { // Must be outside of any component LifeCycle (such as `componentDidMount`). - console.log('handleFirebasePushInForeground'); + //console.log('handleFirebasePushInForeground'); PushNotification.configure({ // (optional) Called when Token is generated (iOS and Android) onRegister: function (token) { //console.log("TOKEN:", token); }, // (required) Called when a remote is received or opened, or local notification is opened onNotification: function (notification) { // process the notification if (notification.userInteraction) { parent.handleFirebasePushInteraction(notification); } else { parent.handleFirebasePush(notification); } // (required) Called when a remote is received or opened, or local notification is opened notification.finish(PushNotificationIOS.FetchResult.NoData); }, // (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android) onAction: function (notification) { console.log("ACTION:", notification.action); console.log("NOTIFICATION:", notification); // process the action }, // (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS) onRegistrationError: function(err) { console.error(err.message, err); }, }); PushNotification.createChannel( { channelId: "sylk-messages", // (required) channelName: "My Sylk stream", // (required) channelDescription: "A channel to receive Sylk Message", // (optional) default: undefined. playSound: false, // (optional) default: true importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); PushNotification.createChannel( { channelId: "sylk-messages-sound", // (required) channelName: "My Sylk stream", // (required) channelDescription: "A channel to receive Sylk Message", // (optional) default: undefined. playSound: true, // (optional) default: true soundName: "default", // (optional) See `soundName` parameter of `localNotification` function importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); PushNotification.deleteChannel("sylk-alert-panel"); PushNotification.createChannel( { channelId: "sylk-alert-panel", // (required) channelName: "Sylk Incoming Calls", // (required) channelDescription: "Display alert panel for incoming calls", // (optional) default: undefined. importance: Importance.MAX, // (optional) default: Importance.HIGH. Int value of the Android notification importance vibrate: true, // (optional) default: true. Creates the default vibration pattern if true. playSound: true, isRingtone: true // soundName: "incallmanager_ringtone.mp3" }, (created) => null // (optional) callback returns whether the channel was created, false means it already existed. ); /* console.log('Available Sylk channels:'); PushNotification.getChannels(function (channel_ids) { console.log(channel_ids); // ['channel_id_1'] }); */ } handleiOSNotification(notification) { // when user touches the system notification and app launches... console.log("Handle iOS push notification:", notification); } postAndroidMessageNotification(uri, content) { //https://www.npmjs.com/package/react-native-push-notification return; console.log('postAndroidMessageNotification'); PushNotification.localNotification({ /* Android Only Properties */ channelId: "sylk-messages", // (required) channelId, if the channel doesn't exist, notification will not trigger. showWhen: true, // (optional) default: true autoCancel: true, // (optional) default: true largeIcon: "ic_launcher", // (optional) default: "ic_launcher". Use "" for no large icon. largeIconUrl: "https://icanblink.com/apple-touch-icon-180x180.png", // (optional) default: undefined smallIcon: "", // (optional) default: "ic_notification" with fallback for "ic_launcher". Use "" for default small icon. bigText: content, // (optional) default: "message" prop subText: "New message", // (optional) default: none //bigPictureUrl: "https://www.example.tld/picture.jpg", // (optional) default: undefined bigLargeIcon: "ic_launcher", // (optional) default: undefined bigLargeIconUrl: "https://www.example.tld/bigicon.jpg", // (optional) default: undefined color: "red", // (optional) default: system default vibrate: true, // (optional) default: true vibration: 100, // vibration length in milliseconds, ignored if vibrate=false, default: 1000 priority: "high", // (optional) set notification priority, default: high ignoreInForeground: true, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting onlyAlertOnce: true, // (optional) alert will open only once with sound and notify, default: false invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true /* iOS and Android properties */ id: 0, // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID title: uri, // (optional) message: content, // (required) //picture: "https://www.example.tld/picture.jpg", // (optional) Display an picture with the notification, alias of `bigPictureUrl` for Android. default: undefined userInfo: {}, // (optional) default: {} (using null throws a JSON value '' error) playSound: false, // (optional) default: true soundName: "default", // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played) number: 10, // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero) repeatType: "day", // (optional) Repeating interval. Check 'Repeating Notifications' section for more info. }); } listenforPushNotifications() { + //console.log('listenforPushNotifications'); if (this.state.appState === null) { this.setState({appState: 'active'}); } else { return; } if (Platform.OS === 'android') { Linking.getInitialURL().then((url) => { if (url) { utils.timestampedLog('Initial external URL: ' + url); this.eventFromUrl(url); } - if (this.state.accountVerified) { - this.changeRoute('/ready', 'start_up'); - } else { - this.changeRoute('/login', 'start_up'); - } }).catch(err => { logger.error({ err }, 'Error getting external URL'); }); firebase.messaging().setBackgroundMessageHandler(async message => { this.handleFirebasePush(message); }); firebase.messaging().getToken() .then(fcmToken => { if (fcmToken) { this._onPushRegistered(fcmToken); } }); Linking.addEventListener('url', this.updateLinkingURL); } else if (Platform.OS === 'ios') { - if (this.state.accountVerified) { - this.changeRoute('/ready', 'start_up'); - } else { - this.changeRoute('/login', 'start_up'); - } - VoipPushNotification.addEventListener('register', this._boundOnPushkitRegistered); VoipPushNotification.registerVoipToken(); PushNotificationIOS.addEventListener('register', this._boundOnPushRegistered); PushNotificationIOS.addEventListener('localNotification', this.onLocalNotification); PushNotificationIOS.addEventListener('notification', this.onRemoteNotification); //let permissions = await checkIosPermissions(); //if (!permissions.alert) { PushNotificationIOS.requestPermissions(); //} } this.boundProximityDetect = this._proximityDetect.bind(this); DeviceEventEmitter.addListener('Proximity', this.boundProximityDetect); AppState.addEventListener('change', this._handleAppStateChange); if (Platform.OS === 'ios') { this._boundOnNotificationReceivedBackground = this._onNotificationReceivedBackground.bind(this); this._boundOnLocalNotificationReceivedBackground = this._onLocalNotificationReceivedBackground.bind(this); VoipPushNotification.addEventListener('notification', this._boundOnNotificationReceivedBackground); VoipPushNotification.addEventListener('localNotification', this._boundOnLocalNotificationReceivedBackground); } else if (Platform.OS === 'android') { this.handleFirebasePushInForeground(this); AppState.addEventListener('focus', this._handleAndroidFocus); AppState.addEventListener('blur', this._handleAndroidBlur); firebase .messaging() .requestPermission() .then(() => { // User has authorised }) .catch(error => { // User has rejected permissions }); this.messageListener = firebase .messaging() .onMessage((message: RemoteMessage) => { // this will just wake up the app to receive // the web-socket invite handled by this.incomingCall() this.handleFirebasePush(message); }); } } handleFirebasePushInteraction(notification) { let data = notification.data; let event = data.event; console.log("handleFirebasePushInteraction", event, data, 'in route', this.currentRoute); const callUUID = data['session-id']; const media = {audio: true, video: data['media-type'] === 'video'}; if (event === 'incoming_conference_request') { if (notification.action === 'Accept') { this.callKeepAcceptCall(callUUID); } else if (notification.action === 'Reject') { this.callKeepRejectCall(callUUID); } else if (notification.action === 'Dismiss') { this.dismissCall(callUUID); } } else if (event === 'incoming_session') { if (notification.action === 'Accept') { this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Video') { this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Audio') { media.video = false; this.callKeepAcceptCall(callUUID, media); } else if (notification.action === 'Reject') { this.callKeepRejectCall(callUUID); } else if (notification.action === 'Dismiss') { this.dismissCall(callUUID); } } else if (event === 'message') { this.initialChatContact = data['from_uri']; } } handleFirebasePush(notification) { let event = notification.data.event; console.log("handleFirebasePush", event); const callUUID = notification.data['session-id']; const from = notification.data['from_uri']; const to = notification.data['to_uri']; const displayName = notification.data['from_display_name']; const outgoingMedia = {audio: true, video: notification.data['media-type'] === 'video'}; const mediaType = notification.data['media-type'] || 'audio'; if (this.unmounted) { //return; } if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); if (!from || !to) { return; } this.postAndroidIncomingCallNotification(notification.data); this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); if (!from) { return; } this.postAndroidIncomingCallNotification(notification.data); this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'cancel') { this.cancelIncomingCall(callUUID); } else if (event === 'message') { console.log('Push notification: new messages on Sylk server from', from); } } notifyIncomingMessageWhileInACall(from) { if (!this.state.selectedContact) { return; } if (this.state.selectedContact.uri !== from) { this._notificationCenter.postSystemNotification('New message from ' + from); this.vibrate(); return; } if (this.state.currentCall && this.state.currentCall.remoteIdentity.uri === from) { this.vibrate(); if (this.currentRoute !== '/ready') { this.goBackToHomeFromCall(); } return; } } sendLocalNotificationWithSound (){ console.log('sendLocalNotificationWithSound'); //PushNotificationIOS.addNotificationRequest({ PushNotificationIOS.presentLocalNotification({ id: 'notificationWithSound', title: 'Sample Title', subtitle: 'Sample Subtitle', body: 'Sample local notification with custom sound', sound: 'customSound.wav', badge: 1, }); }; sendNotification (title, subtitle, body) { DeviceEventEmitter.emit('remoteNotificationReceived', { remote: true, aps: { alert: {title: title, subtitle: subtitle, body: body}, sound: 'default', category: 'REACT_NATIVE', 'content-available': 1, 'mutable-content': 1, }, }); }; sendSilentNotification () { DeviceEventEmitter.emit('remoteNotificationReceived', { remote: true, aps: { category: 'REACT_NATIVE', 'content-available': 1, }, }); }; onRemoteNotification(notification) { const title = notification.getAlert().title; const subtitle = notification.getAlert().subtitle; const body = notification.getAlert().body; const message = notification.getMessage(); const content_available = notification.getContentAvailable(); const category = notification.getCategory(); const badge = notification.getBadgeCount(); const sound = notification.getSound(); const isClicked = notification.getData().userInteraction === 1; //console.log('Got remote notification', title, subtitle, body); this.sendLocalNotification(title + ' ' + subtitle, body); }; sendLocalNotification (title, body) { PushNotificationIOS.presentLocalNotification({ alertTitle: title, alertBody: body }); }; onLocalNotification(notification) { //console.log('Got local notification', notification); this.updateTotalUread(); }; cancelIncomingCall(callUUID) { if (this.unmounted) { return; } this.hideInternalAlertPanel('cancel'); if (this.callKeeper._acceptedCalls.has(callUUID)) { return; } utils.timestampedLog('Push notification: cancel call', callUUID); let call = this.callKeeper._calls.get(callUUID); if (!call) { if (!this.callKeeper._cancelledCalls.has(callUUID)) { utils.timestampedLog('Cancel incoming call that did not arrive on web socket', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); if (this.startedByPush) { this.resetStartedByPush('cancelIncomingCall') if (this.currentRoute) { this.changeRoute('/ready', 'incoming_call_cancelled'); - } else { - this.changeRoute('/login', 'start_up'); } } this.updateLoading(null, 'cancel_incoming_call'); } return; } if (call.state === 'incoming') { utils.timestampedLog('Cancel incoming call that was not yet accepted', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); if (this.startedByPush) { if (this.currentRoute) { this.changeRoute('/ready', 'incoming_call_cancelled'); - } else { - this.changeRoute('/login', 'start_up'); } } } } _proximityDetect(data) { //utils.timestampedLog('Proximity changed, isNear is', data.isNear); if (!this.state.proximityEnabled) { return; } if (data.isNear) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } startCallWhenReady(targetUri, options) { this.resetGoToReadyTimer(); if (options.conference) { this.startConference(targetUri, options); } else { this.startCall(targetUri, options); } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } _onPushkitRegistered(token) { this.pushkittoken = token; } _onPushRegistered(token) { this.pushtoken = token; } _sendPushToken(account) { if ((this.pushtoken && !this.tokenSent)) { let token = null; //console.log('_sendPushToken this.pushtoken', this.pushtoken); if (Platform.OS === 'ios') { token = `${this.pushkittoken}-${this.pushtoken}`; } else if (Platform.OS === 'android') { token = this.pushtoken; } utils.timestampedLog('Push token for app', bundleId, 'sent'); account.setDeviceToken(token, Platform.OS, deviceId, true, bundleId); this.tokenSent = true; } } _handleAndroidFocus = nextFocus => { //utils.timestampedLog('----- APP in focus'); if (Platform.OS === 'ios') { PushNotificationIOS.cancelLocalNotifications(); } else { PushNotification.cancelAllLocalNotifications(); } this.setState({inFocus: true}); this.refreshNavigationItems(); this.fetchSharedItems(); - this.respawnConnection(); } fetchSharedItems() { - return; - - console.log('fetchSharedItems...'); ReceiveSharingIntent.getReceivedFiles(files => { // files returns as JSON Array example //[{ filePath: null, text: null, weblink: null, mimeType: null, contentUri: null, fileName: null, extension: null }] - console.log(files); if (files.length > 0) { console.log('Will share to contacts', files); this.setState({shareToContacts: true, shareContent: files}); let item = files[0]; let what = 'Share text with contacts'; if (item.weblink) { what = 'Share web link with contacts'; } if (item.filePath) { what = 'Share file with contacts'; } this._notificationCenter.postSystemNotification(what); } else { console.log('Nothing to share'); } }, (error) =>{ - console.log(error); + console.log('Error receiving sharing intent', error.message); }, - 'ShareMedia' // share url protocol (must be unique to your app, suggest using your apple bundle id) + 'com.agprojects.sylk' // share url protocol (must be unique to your app, suggest using your apple bundle id) ); } refreshNavigationItems() { var todayStart = new Date(); todayStart.setHours(0,0,0,0); var yesterdayStart = new Date(); yesterdayStart.setDate(yesterdayStart.getDate() - 2); yesterdayStart.setHours(0,0,0,0); let today = false; let yesterday = false; let conference = false; let navigationItems = this.state.navigationItems; Object.keys(this.state.myContacts).forEach((key) => { if (this.state.myContacts[key].tags.indexOf('conference') > -1 || this.state.myContacts[key].conference) { conference = true; } if (this.state.myContacts[key].timestamp > todayStart) { today = true; } if (this.state.myContacts[key].timestamp > yesterdayStart && this.state.myContacts[key].timestamp < todayStart) { yesterday = true; } }); navigationItems = {today: today, yesterday: yesterday, conference: conference}; this.setState({navigationItems: navigationItems}); } _handleAndroidBlur = nextBlur => { //utils.timestampedLog('----- APP out of focus'); this.setState({inFocus: false}); } _handleAppStateChange = nextAppState => { //utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (nextAppState === this.state.appState) { return; } if (this.callKeeper.countCalls === 0 && !this.state.outgoingCallUUID) { /* utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (this.callKeeper.countCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countCalls, 'calls'); } if (this.callKeeper.countPushCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countPushCalls, 'push calls'); } if (this.startedByPush) { utils.timestampedLog('- APP state changed, started by push in', nextAppState, 'state'); } if (this.state.connection) { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState, 'with connection', Object.id(this.state.connection)); } else { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState); } */ } if (this.state.appState === 'background' && nextAppState === 'active') { this.respawnConnection(nextAppState); } this.setState({appState: nextAppState}); } respawnConnection(state) { if (!this.state.connection) { utils.timestampedLog('Web socket does not exist'); } else if (!this.state.connection.state) { utils.timestampedLog('Web socket is waiting for connection...'); } else { /* if (this.state.connection.state !== 'ready' && this.state.connection.state !== 'connecting') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'reconnecting because', this.state.connection.state); this.state.connection.reconnect(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'new state is', this.state.connection.state); } */ } if (this.state.account) { if (!this.state.connection) { utils.timestampedLog('Active account without connection removed'); this.setState({account: null}); } } else { utils.timestampedLog('No active account'); } if (this.state.accountId && (!this.state.connection || !this.state.account)) { this.handleRegistration(this.state.accountId, this.state.password); } } closeConnection(reason='unmount') { if (!this.state.connection) { return; } if (!this.state.account && this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.connection.close(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.setState({connection: null, account: null}); } else if (this.state.connection && this.state.account) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.account.removeListener('outgoingCall', this.outgoingCall); this.state.account.removeListener('conferenceCall', this.outgoingConference); this.state.account.removeListener('incomingCall', this.incomingCallFromWebSocket); this.state.account.removeListener('missedCall', this.missedCall); this.state.account.removeListener('conferenceInvite', this.conferenceInviteFromWebSocket); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { utils.timestampedLog('Failed to remove account:', error); } else { //utils.timestampedLog('Account removed'); } if (this.state.connection) { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.state.connection.close(); } this.setState({connection: null, account: null}); } ); } else { this.setState({connection: null, account: null}); } } startCallFromCallKeeper(data) { utils.timestampedLog('Starting call from OS...'); let callUUID = data.callUUID || uuid.v4(); let is_conf = data.handle.search('videoconference.') === -1 ? false: true; this.backToForeground(); if (is_conf) { this.callKeepStartConference(data.handle, {audio: true, video: data.video || true, callUUID: callUUID}); } else { this.callKeepStartCall(data.handle, {audio: true, video: data.video, callUUID: callUUID}); } this._notificationCenter.removeNotification(); } selectContact(contact, origin='') { if (contact !== this.state.selectedContact) { this.setState({pinned: false}); } this.setState({selectedContact: contact}); this.initialChatContact = null; } logTimeline(step) { return; let diff = Math.floor((new Date() - this.startTimestamp) / 1000); console.log('Timeline:', step, diff); } connectionStateChanged(oldState, newState) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Web socket', connection, 'state changed:', oldState, '->' , newState); } this.logTimeline('connection ' + newState); switch (newState) { case 'closed': this.syncRequested = false; if (this.state.connection) { utils.timestampedLog('Web socket was terminated'); this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this._notificationCenter.postSystemNotification('Connection lost'); } //this.setState({connection: null, account: null}); this.setState({account: null}); break; case 'ready': this._notificationCenter.removeNotification(); this.updateLoading(null, 'ready'); if (this.state.autoLogin) { this.processRegistration(this.state.accountId, this.state.password); this.callKeeper.setAvailable(true); } break; case 'disconnected': this.syncRequested = false; if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { this.hangupCall(this.state.currentCall.id, 'outgoing_connection_failed'); } if (this.state.incomingCall) { this.hangupCall(this.state.incomingCall.id, 'connection_failed'); } this.setState({ registrationState: 'failed', generatedVideoTrack: false, }); if (this.currentRoute === '/login' && this.state.registrationKeepalive) { this.changeRoute('/ready', 'websocket disconnected'); } break; default: if (this.state.registrationKeepalive && !this.state.accountVerified) { - this.updateLoading('Connecting...', 'connection'); + //this.updateLoading('Connecting...', 'connection'); } break; } } notificationCenter() { return this._notificationCenter; } showRegisterFailure(reason) { const connection = this.getConnection(); utils.timestampedLog('Registration error: ' + reason, 'on web socket', connection); this.setState({ registrationState: 'failed', status : { msg : 'Sign In failed: ' + reason, level : 'danger' } }); this.updateLoading(null, 'show_register_failure'); if (this.startedByPush) { // TODO: hangup incoming call } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'register failure'); } } registrationStateChanged(oldState, newState, data) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Registration state changed:', oldState, '->', newState, 'on web socket', connection); } if (!this.state.account) { utils.timestampedLog('Account', this.state.accountId, 'is disabled'); this.updateLoading(null, 'account_disabled'); return; } if (newState === 'failed') { let reason = data.reason; if (reason.indexOf('904') > -1) { // Sofia SIP: WAT reason = 'Wrong account or password'; } else if (reason === 408) { reason = 'Timeout'; } this.showRegisterFailure(reason); if (this.state.registrationKeepalive) { if (this.state.connection !== null && this.state.connection.state === 'ready') { utils.timestampedLog('Retry to register...'); this.state.account.register(); } } else { // add a timer to retry register after awhile if (reason >= 500 || reason === 408) { utils.timestampedLog('Retry to register after 5 seconds delay...'); setTimeout(this.state.account.register(), 5000); } else { if (this.registrationFailureTimer) { utils.timestampedLog('Cancel registration timer'); clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } } } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'register failed'); } } else if (newState === 'registered') { if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } if (!this.state.accountVerified) { this.loadSylkContacts(); } /* setTimeout(() => { this.updateServerHistory() }, 1000); */ if (this.state.enrollment) { let myContacts = this.state.myContacts; myContacts[this.state.account.id] = this.newContact(this.state.account.id, this.state.displayName); this.saveSylkContact(this.state.account.id, myContacts[this.state.account.id], 'enrollment'); } if (this.mustSendPublicKey) { var uri = uuid.v4() + '@' + this.state.defaultDomain; console.log('Send 1st public to', uri); this.sendPublicKey(uri); this.mustSendPublicKey = false; } storage.set('account', { accountId: this.state.account.id, password: this.state.password, verified: true }); this.setState({accountVerified: true, enrollment: false, autoLogin: true, registrationKeepalive: true, registrationState: 'registered' }); this.updateLoading(null, 'registered'); this.requestSyncConversations(this.state.lastSyncId); this.replayJournal(); //if (this.currentRoute === '/login' && (!this.startedByPush || Platform.OS === 'ios')) { // TODO if the call does not arrive, we never get back to ready if (this.currentRoute === '/login') { this.changeRoute('/ready', 'registered'); } return; } else { this.setState({status: null, registrationState: newState }); } if (this.mustLogout) { this.logout(); } } showAlertPanel(data) { console.log('Show alert panel'); if (this.callKeeper._cancelledCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._terminatedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._acceptedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } if (this.callKeeper._rejectedCalls.has(data.callUUID)) { console.log('Show internal alert panel cancelled'); return; } let contact; let media = {audio: true, video: false}; let callId; let from; let displayName; if ('from_display_name' in data && 'from_uri' in data) { // Firebase notification from = data.from_uri; displayName = data.from_display_name; callId = data['session-id']; if (data['media-type'] === 'video') { media.video = true; } } else if (data.hasOwnProperty('_remoteIdentity')) { // Sylk call object from = data.remoteIdentity.uri; displayName = data.remoteIdentity.displayName; callId = data.id; if (data.mediaTypes && data.mediaTypes.video) { media.video = true; } } else { console.log('Missing contact data for Alert panel'); return; } if (from in this.state.myContacts) { contact = this.state.myContacts[from]; } else { let contacts = this.lookupContacts(from); if (contacts.length > 0) { contact = this.newContact(from, contacts[0].name); } } if (!contact) { contact = this.newContact(from, displayName); } if (!callId) { console.log('Missing callId for Alert panel'); return; } this.setState({incomingCallUUID: callId, incomingContact: contact, incomingMedia: media }); } postAndroidIncomingCallNotification(data) { //console.log('postAndroidIncomingCallNotification', data); if (Platform.OS !== 'android') { return; } if (this.callKeeper.selfManaged) { this.showAlertPanel(data); return; } let media = {audio: true, video: data['media-type'] === 'video'}; let from = data.from_display_name || data.from_uri; if (data.from_display_name && data.from_display_name != data.from_uri) { from = data.from_display_name + ' (' + data.from_uri + ')'; } console.log('Show Android incoming call notification', from, media); let actions = ['Audio']; if (media.video) { actions.push('Video'); } actions.push('Reject'); actions.push('Dismiss'); PushNotification.localNotification({ /* Android Only Properties */ channelId: "sylk-alert-panel", // (required) channelId, if the channel doesn't exist, notification will not trigger. vibrate: true, // (optional) default: true priority: "max", // (optional) set notification priority, default: high ongoing: true, loopSound: true, fullScreen: true, ignoreInForeground: false, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true actions: actions, /* iOS and Android properties */ title: 'Incoming call', // (optional) message: 'From ' + from, // (required) //picture: "https://www.example.tld/picture.jpg", // (optional) Display an picture with the notification, alias of `bigPictureUrl` for Android. default: undefined userInfo: data, // (optional) default: {} (using null throws a JSON value '' error) playSound: true, // (optional) default: true number: 10, // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero) }); } playIncomingRingtone(callUUID, force=false) { if (!this.callKeeper.selfManaged) { console.log('playIncomingRingtone skip because we are not self managed....'); return; } if (this.callKeeper._cancelledCalls.has(callUUID)) { console.log('playIncomingRingtone cancelled for', callUUID); return; } if (this.cancelRingtoneTimer) { clearTimeout(this.cancelRingtoneTimer); this.cancelRingtoneTimer = null; } else { console.log('Play local ringtone and vibrate'); Vibration.vibrate(VIBRATION_PATTERN, true); InCallManager.startRingtone('_DEFAULT_'); } this.cancelRingtoneTimer = setTimeout(() => { console.log('Cancel ringtones by timer') this.stopRingtones(); }, 60000); } stopRingtones() { if (this.cancelRingtoneTimer) { clearTimeout(this.cancelRingtoneTimer); this.cancelRingtoneTimer = null; } InCallManager.stopRingtone(); Vibration.cancel(); } hideInternalAlertPanel(by=null) { //console.log('hideInternalAlertPanel by', by); this.stopRingtones(); this.setState({incomingContact: null, incomingMedia: null}); } vibrate() { Vibration.vibrate(VIBRATION_PATTERN, true); setTimeout(() => { Vibration.cancel(); }, 1000); } heartbeat() { if (this.unmounted) { return; } this.heartbeats = this.heartbeats + 1; if (this.heartbeats % 40 == 0) { this.trimLogs(); } if (this.state.connection) { //console.log('Check calls in', this.state.appState, 'with connection', Object.id(this.state.connection), this.state.connection.state); } else { //console.log('Check calls in', this.state.appState, 'with no connection'); } let callState; if (this.state.currentCall && this.state.incomingCall && this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog('We have an incoming call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.incomingCall) { //utils.timestampedLog('We have an incoming call:', this.state.incomingCall ? (this.state.incomingCall.id + ' ' + this.state.incomingCall.state): 'None'); callState = this.state.incomingCall.state; } else if (this.state.currentCall) { //utils.timestampedLog('We have an outgoing call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.outgoingCallUUID) { //utils.timestampedLog('We have a pending outgoing call:', this.state.outgoingCallUUID); } else { //utils.timestampedLog('We have no calls'); if (this.state.appState === 'background' && this.state.connection && this.state.connection.state === 'ready') { //this.closeConnection('background with no calls'); } } this.callKeeper.heartbeat(); } stopRingback() { //utils.timestampedLog('Stop ringback'); InCallManager.stopRingback(); } resetGoToReadyTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; } } goToReadyNowAndCancelTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; this.changeRoute('/ready', 'cancel_timer_incoming_call'); } } isConference(call) { const _call = call || this.state.currentCall; if (_call && _call.hasOwnProperty('_participants')) { return true; } return false; } callStateChanged(oldState, newState, data) { if (this.unmounted) { return; } // outgoing accepted: null -> progress -> accepted -> established -> terminated // outgoing accepted: null -> progress -> established -> accepted -> terminated (with early media) // incoming accepted: null -> incoming -> accepted -> established -> terminated // 2nd incoming call is automatically rejected by sylkrtc library /* utils.timestampedLog('---currentCall start:', this.state.currentCall); utils.timestampedLog('---incomingCall start:', this.state.incomingCall); */ let call = this.callKeeper._calls.get(data.id); if (!call) { utils.timestampedLog("callStateChanged error: call", data.id, 'not found in callkeep manager'); return; } let callUUID = call.id; const connection = this.getConnection(); utils.timestampedLog('Sylkrtc call', callUUID, 'state change:', oldState, '->', newState, 'on web socket', connection); this.logTimeline('call ' + newState); /* if (newState === 'established' || newState === 'accepted') { // restore the correct UI state if it has transitioned illegally to /ready state if (call.hasOwnProperty('_participants')) { this.changeRoute('/conference', 'correct call state'); } else { this.changeRoute('/call', 'correct call state'); } } */ let newCurrentCall; let newincomingCall; let direction = call.direction; let hasVideo = false; let mediaType = 'audio'; let tracks; let readyDelay = 5000; if (this.state.incomingCall && this.state.currentCall) { if (newState === 'terminated') { if (this.state.incomingCall == this.state.currentCall) { newCurrentCall = null; newincomingCall = null; } if (this.state.incomingCall.id === call.id) { if (oldState === 'incoming') { //utils.timestampedLog('Call state changed:', 'incoming call must be cancelled'); this.hideInternalAlertPanel(newState); } if (oldState === 'established' || oldState === 'accepted') { //utils.timestampedLog('Call state changed:', 'incoming call ended'); this.hideInternalAlertPanel(newState); } // new call must be cancelled newincomingCall = null; newCurrentCall = this.state.currentCall; } if (this.state.currentCall != this.state.incomingCall && this.state.currentCall.id === call.id) { if (oldState === 'established' || newState === 'accepted') { //utils.timestampedLog('Call state changed:', 'outgoing call must be hangup'); // old call must be closed } newCurrentCall = null; newincomingCall = this.state.incomingCall; } } else if (newState === 'accepted') { if (this.state.incomingCall === this.state.currentCall) { newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { newCurrentCall = this.state.currentCall; } this.backToForeground(); } else if (newState === 'established') { if (this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog("Incoming call media started"); newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { //utils.timestampedLog("Outgoing call media started"); newCurrentCall = this.state.currentCall; } } else { //utils.timestampedLog('Call state changed:', 'We have two calls in unclear state'); } } else if (this.state.incomingCall) { //this.backToForeground(); //utils.timestampedLog('Call state changed: We have one incoming call'); newincomingCall = this.state.incomingCall; newCurrentCall = this.state.incomingCall; if (this.state.incomingCall.id === call.id) { if (newState === 'terminated') { if (this.startedByPush) { this.resetStartedByPush('terminated') this.requestSyncConversations(this.state.lastSyncId); } //utils.timestampedLog("Incoming call was cancelled"); this.hideInternalAlertPanel(newState); newincomingCall = null; newCurrentCall = null; readyDelay = 10; } else if (newState === 'accepted') { //utils.timestampedLog("Incoming call was accepted"); this.hideInternalAlertPanel(newState); this.backToForeground(); } else if (newState === 'established') { //utils.timestampedLog("Incoming call media started"); this.hideInternalAlertPanel(newState); } } } else if (this.state.currentCall) { //utils.timestampedLog('Call state changed: We have one current call'); newCurrentCall = newState === 'terminated' ? null : call; newincomingCall = null; if (newState !== 'terminated') { this.setState({reconnectingCall: false}); } } else { newincomingCall = null; newCurrentCall = null; } /* utils.timestampedLog('---currentCall:', newCurrentCall); utils.timestampedLog('---incomingCall:', newincomingCall); */ let callsState; switch (newState) { case 'progress': //this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; if (!this.isConference(call)){ InCallManager.startRingback('_BUNDLE_'); if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } else { this.speakerphoneOn(); } break; case 'early-media': //this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.stopRingback(); break; case 'established': callsState = this.state.callsState; callsState[callUUID] = {startTime: new Date()}; this.setState({callsState: callsState}); this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; InCallManager.start({media: mediaType}); if (direction === 'outgoing') { this.stopRingback(); if (this.state.speakerPhoneEnabled) { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } else { if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } break; case 'accepted': callsState = this.state.callsState; callsState[callUUID] = {startTime: new Date()}; this.setState({callsState: callsState}); if (direction === 'incoming') { this.callKeeper.setCurrentCallActive(callUUID); if (this.timeoutIncomingTimer) { clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } } if (callUUID === this.state.incomingCallUUID) { this.updateLoading(null, 'incoming_call'); } this.setState({incomingCallUUID: null}); this.backToForeground(); this.resetGoToReadyTimer(); if (direction === 'outgoing') { this.stopRingback(); } break; case 'terminated': let startTime; if (callUUID in this.state.callsState) { callsState = this.state.callsState; startTime = callsState[callUUID].startTime; delete callsState[callUUID]; this.setState({callsState: callsState}); } if (callUUID === this.state.incomingCallUUID) { this.setState({incomingCallUUID: null, incomingContact: null}); this.updateLoading(null, 'incoming_call'); } this._terminatedCalls.set(callUUID, true); utils.timestampedLog(callUUID, direction, 'terminated with reason', data.reason); if (this.state.incomingCall && this.state.incomingCall.id === call.id) { newincomingCall = null; } if (this.state.currentCall && this.state.currentCall.id === call.id) { newCurrentCall = null; } let callSuccesfull = false; let reason = data.reason; let play_busy_tone = !this.isConference(call); let CALLKEEP_REASON; let missed = false; let cancelled = false; let server_failure = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; if (!reason || reason.match(/200/)) { if (oldState === 'progress' && direction === 'outgoing') { reason = 'Cancelled'; cancelled = true; play_busy_tone = false; } else if (oldState === 'incoming') { reason = 'Cancelled'; missed = true; play_busy_tone = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else { reason = 'Hangup'; callSuccesfull = true; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } } else if (reason.match(/402/)) { reason = 'Payment required'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/403/)) { //reason = 'Forbidden'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/404/)) { reason = 'User not found'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/408/)) { reason = 'No answer'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/482/)) { reason = 'Loop detected'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/480/)) { reason = 'Is not online'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else if (reason.match(/486/)) { reason = 'Is busy'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/603/)) { reason = 'Cannot answer now'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/487/)) { reason = 'Cancelled'; play_busy_tone = false; cancelled = true; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } else if (reason.match(/488/)) { reason = 'Unacceptable media'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/4\d\d/)) { reason = 'Call failure: ' + reason; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/[5|6]\d\d/)) { reason = 'Server failure: ' + reason; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; server_failure = true; } else if (reason.match(/904/)) { // Sofia SIP: WAT reason = 'Wrong account or password'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else { server_failure = true; } if (play_busy_tone) { this.playBusyTone(); } if (direction === 'outgoing') { this.setState({declineReason: reason}); } this.stopRingback(); let msg; let current_datetime = new Date(); let formatted_date = utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); let diff = 0; if (startTime) { let duration = moment.duration(new Date() - startTime); diff = Math.floor((new Date() - startTime) / 1000); if (diff > 3600) { duration = duration.format('hh:mm:ss', {trim: false}); } else { duration = duration.format('mm:ss', {trim: false}); } msg = formatted_date + " - " + direction +" " + mediaType + " call ended after " + duration; this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); } else { msg = formatted_date + " - " + direction +" " + mediaType + " call ended (" + reason + ")"; if (!server_failure) { this.saveSystemMessage(call.remoteIdentity.uri.toLowerCase(), msg, direction, missed); if (reason.indexOf('PSTN calls forbidden') > -1) { setTimeout(() => { this.renderPurchasePSTNCredit(call.remoteIdentity.uri.toLowerCase()); }, 2000); } } } this.terminateSsiConnections(call.remoteIdentity.uri.toLowerCase()); this.updateHistoryEntry(call.remoteIdentity.uri.toLowerCase(), callUUID, diff); this.callKeeper.endCall(callUUID, CALLKEEP_REASON); if (play_busy_tone && oldState !== 'established' && direction === 'outgoing') { this._notificationCenter.postSystemNotification('Call ended:', {body: reason}); } break; default: break; } /* utils.timestampedLog('---currentCall end:', newCurrentCall); utils.timestampedLog('---incomingCall end:', newincomingCall); */ this.setState({ currentCall: newCurrentCall, incomingCall: newincomingCall }); if (!this.state.currentCall && !this.state.incomingCall) { this.speakerphoneOn(); if (!this.state.reconnectingCall) { if (this.state.inFocus) { if (this.currentRoute !== '/ready') { utils.timestampedLog('Will go to ready in', readyDelay/1000, 'seconds (terminated)', callUUID); this.goToReadyTimer = setTimeout(() => { this.changeRoute('/ready', 'no_more_calls'); }, readyDelay); } } else { if (this.currentRoute !== '/conference') { this.changeRoute('/ready', 'no_more_calls'); } } } } if (this.state.currentCall) { //console.log('Current:', this.state.currentCall.id); } if (this.state.incomingCall) { //console.log('Incoming:', this.state.incomingCall.id); } } async terminateSsiConnections(uri) { if (!this.ssiAgent) { return; } let allConnections = await this.ssiAgent.connections.getAll(); let callConnections = allConnections.filter(x => x.theirLabel.startsWith(uri)); for (const x of callConnections) { utils.timestampedLog('SSI connection', x.id, 'to', uri, 'removed'); await this.ssiAgent.connections.deleteById(x.id); } allConnections = await this.ssiAgent.connections.getAll(); this.setState({ssiConnections: allConnections}); } finishInviteToConference() { this.setState({inviteContacts: false, selectedContacts: []}); } goBackToCall() { let call = this.state.currentCall || this.state.incomingCall; if (call) { if (call.hasOwnProperty('_participants')) { this.changeRoute('/conference', 'back to call'); } else { this.changeRoute('/call', 'back to call'); } //this.getMessages(call.remoteIdentity.uri); } else { console.log('No call to go back to'); } } goBackToHome() { this.changeRoute('/ready', 'back to home'); } goBackToHomeFromCall() { this.changeRoute('/ready', 'back to home'); if (this.state.callContact) { this.setState({selectedContact: this.state.callContact}); if (Object.keys(this.state.messages).indexOf(this.state.callContact.uri) === -1) { this.getMessages(this.state.callContact.uri); } } } goBackToHomeFromConference() { this.changeRoute('/ready', 'back to home'); if (this.state.callContact) { this.setState({selectedContact: this.state.callContact}); if (Object.keys(this.state.messages).indexOf(this.state.callContact.uri) === -1) { this.getMessages(this.state.callContact.uri); } } } inviteToConference() { console.log('Invite contacts to conference...'); this.goBackToHome(); setTimeout(() => { this.setState({inviteContacts: true, selectedContacts: []}); }, 100); } handleEnrollment(account) { console.log('Enrollment for new account', account); this.signup[account.id] = account.email; storage.set('signup', this.signup); storage.set('last_signup', account.id); this.setState({displayName: account.displayName, enrollment: true, email: account.email}); this.handleRegistration(account.id, account.password); } handleRegistration(accountId, password) { //console.log('handleRegistration', accountId, 'verified =', this.state.accountVerified); if (this.state.account !== null && this.state.registrationState === 'registered' ) { return; } this.setState({ accountId : accountId, password : password, }); if (!this.startedByPush) { - this.updateLoading('Connecting...', 'handleRegistration'); + //this.updateLoading('Connecting...', 'handleRegistration'); } if (this.state.accountVerified) { this.loadSylkContacts(); } if (this.state.connection === null) { utils.timestampedLog('Web socket handle registration for', accountId); const userAgent = 'Sylk Mobile'; let connection = sylkrtc.createConnection({server: config.wsServer}); utils.timestampedLog('Web socket', Object.id(connection), 'was opened'); connection.on('stateChanged', this.connectionStateChanged); connection.on('publicKey', this.publicKeyReceived); this.setState({connection: connection}); } else { if (this.state.connection.state === 'ready' && this.state.registrationState !== 'registered') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'handle registration for', accountId); this.processRegistration(accountId, password); } else if (this.state.connection.state !== 'ready') { if (this._notificationCenter) { - this._notificationCenter.postSystemNotification('Waiting for Internet connection'); + //this._notificationCenter.postSystemNotification('Waiting for Internet connection'); } if (this.currentRoute === '/login' && this.state.accountVerified) { this.changeRoute('/ready', 'start_up'); } } } } processRegistration(accountId, password, displayName) { if (!displayName) { displayName = this.state.displayName; } if (!this.state.connection) { return; } utils.timestampedLog('Process registration for', accountId, '(', displayName, ')'); if (this.state.account && this.state.connection) { this.state.connection.removeAccount(this.state.account, (error) => { this.setState({registrationState: null, registrationKeepalive: false}); } ); } const options = { account: accountId, password: password, displayName: displayName || '', incomingHeaderPrefixes: ['SSI'] }; if (this.state.connection._accounts.has(options.account)) { return; } if (this.state.accountVerified) { this.registrationFailureTimer = setTimeout(() => { this.showRegisterFailure('Register timeout'); this.processRegistration(accountId, password); }, 10000); } const account = this.state.connection.addAccount(options, (error, account) => { if (!error) { account.on('outgoingCall', this.outgoingCall); account.on('conferenceCall', this.outgoingConference); account.on('registrationStateChanged', this.registrationStateChanged); account.on('incomingCall', this.incomingCallFromWebSocket); account.on('incomingMessage', this.incomingMessage); account.on('syncConversations', this.syncConversations); account.on('readConversation', this.readConversation); account.on('removeConversation', this.removeConversation); account.on('removeMessage', this.removeMessage); account.on('outgoingMessage', this.outgoingMessage); account.on('messageStateChanged', this.messageStateChanged); account.on('missedCall', this.missedCall); account.on('conferenceInvite', this.conferenceInviteFromWebSocket); //utils.timestampedLog('Web socket account', account.id, 'is ready, registering...'); this._sendPushToken(account); this.setState({account: account}); this.generateKeysIfNecessary(account); account.register(); this.initSSIAgent(); storage.set('account', { accountId: this.state.accountId, password: this.state.password }); } else { this.showRegisterFailure(408); } }); } async initSSIAgent() { // SSI wallet - init agent with wallet Id this.state.accountId if (this.ssiAgent) { // already initialized return; } if (!this.state.ssiRequired) { return; } if (!this.state.accountId) { utils.timestampedLog('Init SSI wallet failed because missing device account id'); return; } if (!this.state.myuuid) { utils.timestampedLog('Init SSI wallet failed because missing device id'); return; } let walletId = this.state.accountId + '_' + this.state.myuuid.replace(/-/g, '_'); - let mediatorUrl = 'wss://ws.didcomm.mediator.bloqzone.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZTUzYWRkMzMtYjZiYS00NWFlLWEwN2MtNTA3NzkxY2YzMjFlIiwgInNlcnZpY2VFbmRwb2ludCI6ICJ3c3M6Ly93cy5kaWRjb21tLm1lZGlhdG9yLmJsb3F6b25lLmNvbSIsICJsYWJlbCI6ICJCbG9xem9uZSBNZWRpYXRvciBBZ2VudCIsICJyZWNpcGllbnRLZXlzIjogWyIzQ2JieUYyVE43RVVTamtTZ3YyNHc2VHZZSGNSZk5yQ3I3eXVTNEJRc1U2RyJdfQ=='; + //let mediatorUrl = 'wss://ws.didcomm.mediator.bloqzone.com?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZTUzYWRkMzMtYjZiYS00NWFlLWEwN2MtNTA3NzkxY2YzMjFlIiwgInNlcnZpY2VFbmRwb2ludCI6ICJ3c3M6Ly93cy5kaWRjb21tLm1lZGlhdG9yLmJsb3F6b25lLmNvbSIsICJsYWJlbCI6ICJCbG9xem9uZSBNZWRpYXRvciBBZ2VudCIsICJyZWNpcGllbnRLZXlzIjogWyIzQ2JieUYyVE43RVVTamtTZ3YyNHc2VHZZSGNSZk5yQ3I3eXVTNEJRc1U2RyJdfQ=='; + let mediatorUrl = 'https://http.mediator.community.animo.id?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiZmFmODZiMjAtZWM4ZC00ZjMzLWI1OGUtN2NmMTAwMzQwZDM5IiwgImxhYmVsIjogIkFuaW1vIENvbW11bml0eSBNZWRpYXRvciIsICJyZWNpcGllbnRLZXlzIjogWyIySzhqUUxaVE15ZkRhcDJnYlczclFMRUhrSml0WjVrQ3R6MVF3NTdWVmlHbSJdLCAic2VydmljZUVuZHBvaW50IjogImh0dHBzOi8vaHR0cC5tZWRpYXRvci5jb21tdW5pdHkuYW5pbW8uaWQifQ=='; utils.timestampedLog('Init SSI wallet id', walletId, 'init via mediator', mediatorUrl); const BCOVRIN_TEST_GENESIS = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}` const agentConfig = { // The label is used for communication with other agents label: walletId, mediatorConnectionsInvite: mediatorUrl, autoAcceptConnections: true, // logger: new ConsoleLogger(LogLevel.debug), autoAcceptCredentials: AutoAcceptCredential.Always, autoAcceptProofs: AutoAcceptProof.Always, walletConfig: { id: walletId, key: 'demo', // this must be autogenerated and stored for each sip account }, indyLedgers: [ { id: 'BCovrin Test', genesisTransactions: BCOVRIN_TEST_GENESIS, isProduction: false, }, ] }; this.ssiAgent = new Agent(agentConfig, agentDependencies); const httpOutboundTransporter = new HttpOutboundTransport(); this.ssiAgent.registerOutboundTransport(httpOutboundTransporter); const WsOutboundTransporter = new WsOutboundTransport(); this.ssiAgent.registerOutboundTransport(WsOutboundTransporter); try { await this.ssiAgent.initialize(); utils.timestampedLog('SSI wallet initialised'); let ssiRoles = this.state.ssiRoles; this.ssiAgent.events.on(CredentialEventTypes.CredentialStateChanged, this.handleSSIAgentCredentialStateChange); this.ssiAgent.events.on(ConnectionEventTypes.ConnectionStateChanged, this.handleSSIAgentConnectionStateChange); this.ssiAgent.events.on(AgentEventTypes.AgentMessageProcessed, this.incomingSsiMessage); if (ssiRoles.indexOf('verifier') === -1) { ssiRoles.push('verifier'); } const credentials = await this.ssiAgent.credentials.getAll(); let hm = credentials.length > 0 ? credentials.length : "no"; utils.timestampedLog('SSI wallet has', hm, 'credentials'); //console.log(credentials); if (credentials.length > 0) { utils.timestampedLog('SSI added holder role'); if (ssiRoles.indexOf('holder') === -1) { ssiRoles.push('holder'); } } this.setState({ssiRoles: ssiRoles}); const allConnections = await this.ssiAgent.connections.getAll(); utils.timestampedLog('SSI wallet has', allConnections.length, 'connections'); //console.log(allConnections); if (self.pendingSsiUrl) { this.handleSSIEnrolment(self.pendingSsiUrl); self.pendingSsiUrl = null; } this.setState({ssiConnections: allConnections}); allConnections.forEach((item) => { utils.timestampedLog('SSI connection', item.id, 'to', item.theirLabel, 'in state', item.state); }); let noCred = credentials.length > 0 ? credentials.length : "no"; //this._notificationCenter.postSystemNotification("SSI wallet initialised with " + noCred + " credentials"); this.setState({ssiCredentials: credentials}); const rmCommunityConnection = async () => { let connections = allConnections.filter(x => x.theirLabel === 'Animo Community Agent') for (const x of connections) { await this.ssiAgent.connections.deleteById(x.id) } connections = allConnections.filter(x => x.theirLabel === 'Animo Community Mediator') for (const x of connections) { await this.ssiAgent.connections.deleteById(x.id) } } //await rmCommunityConnection(); // run only once if (!allConnections.map((x) => x.theirLabel).includes('Animo Community Agent')) { // create a connection to Animo credential issuer, must be done once // connection is saved and reused later when we recreate de agent // once we do have a credential, we don't need to connect anymore await this.initSSIConnection(); } } catch (error) { utils.timestampedLog('SSI wallet init error:', error); this._notificationCenter.postSystemNotification('SSI init' + error); } } async initSSIConnection() { // replaced by the QR code reader return; utils.timestampedLog('SSI connection init'); // this invitation should be obtained from a QR code from the issuer website // this is still demo with hardwired values -adi let url = 'https://didcomm.agent.community.animo.id?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNDdiNDE1ZjEtNDk3OS00OGM0LWI5YTMtYWM2OWZlMGM0ZjZiIiwgInJlY2lwaWVudEtleXMiOiBbIkJBMmt1N3FCQ2toZE5ud3N1cU5GS0ZQa2dNejZoMnA2TENDd2hIaEE3U0twIl0sICJsYWJlbCI6ICJBbmltbyBDb21tdW5pdHkgQWdlbnQiLCAic2VydmljZUVuZHBvaW50IjogImh0dHBzOi8vZGlkY29tbS5hZ2VudC5jb21tdW5pdHkuYW5pbW8uaWQifQ=='; try { const result = await this.ssiAgent.connections.receiveInvitationFromUrl(url); utils.timestampedLog('SSI connection cached'); // now we can receive a credential from the issuer } catch (error) { utils.timestampedLog('SSI connection error', error); } } async handleSSIAgentCredentialStateChange(event) { utils.timestampedLog('SSI wallet Credential State Change', event.payload.credentialRecord.id, event.payload.previousState, '->', event.payload.credentialRecord.state); if (event.payload.credentialRecord.state === CredentialState.OfferReceived) { utils.timestampedLog('SSI credential received:', event.payload.credentialRecord); this._notificationCenter.postSystemNotification("New SSI credential received"); // this is not needed if we are configured to auto accept // this.ssiAgent.credentials.acceptOffer(event.payload.credentialRecord.id); } else if (event.payload.credentialRecord.state === CredentialState.Done) { utils.timestampedLog('SSI wallet credential saved'); this.postSystemNotification('SSI credential saved'); const credentials = await this.ssiAgent.credentials.getAll(); this.setState({ssiCredentials: credentials}); setTimeout(() => { this.filterHistory('ssi'); }, 1000); } } async handleSSIAgentConnectionStateChange(event) { utils.timestampedLog('SSI wallet connection', event.payload.connectionRecord.id, 'state changed to', event.payload.connectionRecord.state); const allConnections = await this.ssiAgent.connections.getAll(); utils.timestampedLog('SSI wallet has', allConnections.length, 'connections'); //console.log(allConnections); this.setState({ssiConnections: allConnections}); if (event.payload.connectionRecord.state === 'complete') { setTimeout(() => { this.filterHistory('ssi'); }, 1000); } } async incomingSsiMessage(event) { if (event.payload.message.type === "https://didcomm.org/basicmessage/1.0/message") { let content = event.payload.message.content; let uri = event.payload.connection.id; let ssiName = uri; let message = new Object(); if (this.state.ssiConnections) { this.state.ssiConnections.forEach((item) => { //console.log('Contacts SSI connection', item); let uri = item.id; if (event.payload.connection.id === item.id) { ssiName = item.theirLabel; return; } }); } console.log('SSI message from', ssiName, ':', content); message.id = event.payload.message.id; message.type = 'normal'; message.contentType = 'text/plain'; message.content = content; message.account = this.state.account; message.ssiName = ssiName; message.timestamp = event.payload.message.sentTime; message.dispositionNotification = []; message.state = 'received'; message.sender = new Object(); message.sender.uri = uri; message.sender.displayName = null; this.incomingMessage(message); } } generateKeysIfNecessary(account) { let keyStatus = this.state.keyStatus; console.log('PGP key generation...'); if ('existsOnServer' in keyStatus) { //console.log('PGP key server was already queried'); // server was queried if (keyStatus['existsOnServer']) { this.setState({keyExistsOnServer: true}) if (keyStatus['existsLocal']) { // key exists in both places if (this.state.keys && keyStatus['serverPublicKey'] !== this.state.keys.public) { console.log('PGP key is different than the one on server'); this.setState({keyDifferentOnServer: true}); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } else { - console.log('PGP key is the same as the one on server'); + console.log('My PGP key is the same as the one on server'); } } else { - console.log('PGP key does not exist'); + console.log('My PGP key does not exist'); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } } else { if (!keyStatus['existsLocal']) { console.log('We have no PGP key here nor on server'); this.generateKeys(); } else { - console.log('PGP key exists local but not on server'); + console.log('My PGP key exists local but not on server'); } } } else { - //console.log('PGP key server was not yet queried'); + console.log('PGP key server was not yet queried'); account.checkIfKeyExists((key) => { keyStatus['serverPublicKey'] = key; - //console.log('PGP key server query done'); + console.log('PGP key server query done'); if (key) { keyStatus['existsOnServer'] = true; this.setState({keyExistsOnServer: true}) if (this.state.keys) { if (this.state.keys && this.state.keys.public !== key) { - console.log('PGP key on server is different than ours'); + console.log('My PGP key on server is different than local'); this.setState({showImportPrivateKeyModal: true, keyDifferentOnServer: true}) } else { - console.log('PGP key exists on server and we have it'); + console.log('My PGP key exists on server and we have a local copy'); keyStatus['existsLocal'] = true; } } else { if (!this.state.contactsLoaded) { console.log('Wait for PGP key until contacts are loaded'); } else { console.log('We have no local PGP key'); setTimeout(() => { this.showImportPrivateKeyModal(); }, 10); } } } else { keyStatus['existsOnServer'] = false; - console.log('PGP key does not exist on server'); + console.log('My PGP key does not exist on server'); if (this.state.contactsLoaded) { - this.generateKeys(); + if (this.state.keys && this.state.keys.private) { + console.log('My PGP public key sent to server'); + this.sendPublicKey(this.state.accountId); + } else { + this.generateKeys(); + } } else { console.log('Wait for PGP key until contacts are loaded'); } } this.setState({keyStatus: keyStatus}); }); } } setDevice(device) { const oldDevices = Object.assign({}, this.state.devices); if (device.kind === 'videoinput') { oldDevices['camera'] = device; } else if (device.kind === 'audioinput') { oldDevices['mic'] = device; } this.setState({devices: oldDevices}); storage.set('devices', oldDevices); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.getLocalMedia(); } getLocalMedia(mediaConstraints={audio: true, video: true}, nextRoute=null) { // eslint-disable-line space-infix-ops let callType = mediaConstraints.video ? 'video': 'audio'; utils.timestampedLog('Get local media for', callType, 'call'); const constraints = Object.assign({}, mediaConstraints); if (constraints.video === true) { if ((nextRoute === '/conference')) { constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; // TODO: remove this, workaround so at least safari works when joining a video conference } else if (nextRoute === '/conference' && isSafari) { constraints.video = false; } else { // ask for 720p video constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; } } logger.debug('getLocalMedia(), (modified) mediaConstraints=%o', constraints); navigator.mediaDevices.enumerateDevices() .then((devices) => { devices.forEach((device) => { //console.log(device); if ('video' in constraints && 'camera' in this.state.devices) { if (constraints.video && constraints.video !== false && (device.deviceId === this.state.devices.camera.deviceId || device.label === this.state.devices.camera.label)) { constraints.video.deviceId = { exact: device.deviceId }; } } if ('mic' in this.state.devices) { if (device.deviceId === this.state.devices.mic.deviceId || device.label === this.state.devices.mic.Label) { // constraints.audio = { // deviceId: { // exact: device.deviceId // } // }; } } }); }) .catch((error) => { utils.timestampedLog('Error: device enumeration failed:', error); }) .then(() => { return navigator.mediaDevices.getUserMedia(constraints) }) .then((localStream) => { clearTimeout(this.loadScreenTimer); //utils.timestampedLog('Local media acquired'); this.setState({localMedia: localStream}); if (nextRoute !== null) { this.changeRoute(nextRoute); } }) .catch((error) => { utils.timestampedLog('Access to local media failed, trying audio only', error); navigator.mediaDevices.getUserMedia({ audio: true, video: false }) .then((localStream) => { clearTimeout(this.loadScreenTimer); if (nextRoute !== null) { this.changeRoute(nextRoute, 'local media aquired'); } }) .catch((error) => { utils.timestampedLog('Access to local media failed:', error); clearTimeout(this.loadScreenTimer); this._notificationCenter.postSystemNotification("Can't access camera or microphone"); this.updateLoading(null, 'get_media'); this.changeRoute('/ready', 'local media failure'); }); }); } getConnection() { return this.state.connection ? Object.id(this.state.connection): null; } showConferenceModal() { Keyboard.dismiss(); this.setState({showConferenceModal: true}); } hideConferenceModal() { this.setState({showConferenceModal: false}); } async callKeepStartConference(targetUri, options={audio: true, video: true, participants: []}) { if (!targetUri) { return; } this.backToForeground(); this.resetGoToReadyTimer(); const micAllowed = await this.requestMicPermission(); if (!micAllowed) { console.log('Cannot start call without access to microphone'); return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } let callUUID = options.callUUID || uuid.v4(); let participants = options.participants || null; if (!options.skipHistory) { this.addHistoryEntry(targetUri, callUUID); } let participantsToInvite = []; if (participants) { participants.forEach((participant_uri) => { if (participant_uri === this.state.accountId) { return; } participantsToInvite.push(participant_uri); }); } this.outgoingMedia = options; this.setState({outgoingCallUUID: callUUID, reconnectingCall: false, callContact: this.state.selectedContact, participantsToInvite: participantsToInvite }); const media = options.video ? 'video' : 'audio'; if (participantsToInvite) { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri, 'with', participantsToInvite); } else { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri); } this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, conference: true, callUUID: callUUID}); } updateSelection(uri) { //console.log('updateSelection', uri); let selectedContacts = this.state.selectedContacts; //console.log('selectedContacts', selectedContacts); let idx = selectedContacts.indexOf(uri); if (idx === -1) { selectedContacts.push(uri); } else { selectedContacts.splice(idx, 1); } this.setState({selectedContacts: selectedContacts}); } async callKeepStartCall(targetUri, options) { this.resetGoToReadyTimer(); targetUri = targetUri.trim().toLowerCase(); if (targetUri.indexOf('@') === -1) { targetUri = targetUri + '@' + this.state.defaultDomain; } const micAllowed = await this.requestMicPermission(); if (!micAllowed) { console.log('Cannot start call without access to microphone'); return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } let callUUID = options.callUUID || uuid.v4(); this.setState({outgoingCallUUID: callUUID, reconnectingCall: false}); utils.timestampedLog('User will start call', callUUID, 'to', targetUri); this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, callUUID: callUUID}); setTimeout(() => { if (this.state.currentCall && this.state.currentCall.id === callUUID && this.state.currentCall.state === 'progress') { this.hangupCall(callUUID, 'cancelled_call'); } }, 60000); } startCall(targetUri, options) { this.setState({targetUri: targetUri, callContact: this.state.selectedContact}); this.getLocalMedia(Object.assign({audio: true, video: options.video}, options), '/call'); } timeoutCall(callUUID, uri) { utils.timestampedLog('Timeout answering call', callUUID); this.addHistoryEntry(uri, callUUID, direction='incoming'); this.forceUpdate(); } closeLocalMedia() { if (this.state.localMedia != null) { utils.timestampedLog('Close local media'); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.setState({localMedia: null}); } } async callKeepAcceptCall(callUUID, options={}) { // called from user interaction with Old alert panel // options used to be media to accept audio only but native panels do not have this feature this.hideInternalAlertPanel('accept'); const micAllowed = await this.requestMicPermission(); if (!micAllowed) { return; } if (options.video) { const cameraAllowed = await this.requestCameraPermission(); if (!cameraAllowed) { options.video = false; } } this.logTimeline('accept call'); this.backToForeground(); this.callKeeper.acceptCall(callUUID, options); this.updateLoading(incomingCallLabel, 'incoming_call'); if (this.timeoutIncomingTimer) { clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } this.timeoutIncomingTimer = setTimeout(() => { this.updateLoading(null, 'incoming_call_timeout'); }, 45000); // TODO this timer must be cancelled if call arrives -adi } callKeepRejectCall(callUUID) { // called from user interaction with Old alert panel utils.timestampedLog('CallKeep will reject call', callUUID); this.hideInternalAlertPanel('reject'); this.callKeeper.rejectCall(callUUID); } dismissCall(callUUID) { // called from user interaction with Old alert panel this.hideInternalAlertPanel('dismiss'); } acceptCall(callUUID, options={}) { console.log('User accepted call', callUUID, options); this.hideInternalAlertPanel('accept'); this.backToForeground(); this.resetGoToReadyTimer(); if (this.state.currentCall) { utils.timestampedLog('Will hangup current call first'); this.hangupCall(this.state.currentCall.id, 'accept_new_call'); // call will continue after transition to /ready } else { utils.timestampedLog('Will get local media now'); let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; if ('video' in options) { hasVideo = hasVideo && options.video; } this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } rejectCall(callUUID) { // called by Call Keep when user rejects call utils.timestampedLog('User rejected call', callUUID); this.hideInternalAlertPanel('reject'); if (!this.state.currentCall) { this.changeRoute('/ready', 'rejected'); } if (this.state.incomingCall && this.state.incomingCall.id === callUUID) { utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', this.state.incomingCall.state, 'state'); this.state.incomingCall.terminate(); } } hangupCall(callUUID, reason) { utils.timestampedLog('Call', callUUID, 'hangup with reason:', reason); let call = this.callKeeper._calls.get(callUUID); let direction = null; let targetUri = null; if (call) { let direction = call.direction; utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', call.state, 'state'); call.terminate(); this.vibrate(); } if (this.busyToneInterval) { clearInterval(this.busyToneInterval); this.busyToneInterval = null; } if (reason === 'user_cancel_call' || reason === 'user_hangup_call' || reason === 'answer_failed' || reason === 'callkeep_hangup_call' || reason === 'accept_new_call' || reason === 'stop_preview' || reason === 'escalate_to_conference' || reason === 'user_hangup_conference_confirmed' || reason === 'timeout' || reason === 'outgoing_connection_failed' ) { this.setState({inviteContacts: false}); this.changeRoute('/ready', reason); if (reason === 'user_hangup_conference_confirmed') { if (this.conferenceEndedTimer) { console.log('Clear timer conferenceEndedTimer'); clearTimeout(this.conferenceEndedTimer); this.conferenceEndedTimer = null; } else { console.log('Clear timer conferenceEndedTimer is not needed'); } } } else if (reason === 'user_hangup_conference') { if (!this.conferenceEndedTimer ) { utils.timestampedLog('Save conference maybe?'); this.conferenceEndedTimer = setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } } else if (reason === 'user_cancelled_conference') { if (!this.conferenceEndedTimer ) { utils.timestampedLog('Save conference maybe?'); this.conferenceEndedTimer = setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } } else { utils.timestampedLog('Will go to ready in 6 seconds (hangup)'); setTimeout(() => { this.changeRoute('/ready', reason); }, 6000); } } playBusyTone() { //utils.timestampedLog('Play busy tone'); InCallManager.stop({busytone: '_BUNDLE_'}); } callKeepSendDtmf(digits) { utils.timestampedLog('Send DTMF', digits); if (this.state.currentCall) { this.callKeeper.sendDTMF(this.state.currentCall.id, digits); } } toggleProximity() { storage.set('proximityEnabled', !this.state.proximityEnabled); if (!this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } this.setState({proximityEnabled: !this.state.proximityEnabled}); } toggleMute(callUUID, muted) { if (this.state.muted != muted) { utils.timestampedLog('Toggle mute for call', callUUID, ':', muted); this.callKeeper.setMutedCall(callUUID, muted); this.setState({muted: muted}); } } async hideImportPrivateKeyModal() { this.setState({privateKey: null, privateKeyImportStatus: '', privateKeyImportSuccess: false, showImportPrivateKeyModal: false}); if (!this.state.keys && Object.keys(this.state.myContacts).length === 0) { this.addTestContacts(); } } async showImportPrivateKeyModal(force=false) { let keyStatus = this.state.keyStatus; if (force) { console.log('Force show PGP key import'); this.setState({showImportPrivateKeyModal: true}); } else { if ('existsOnServer' in keyStatus) { if ('existsLocal' in keyStatus) { if (!keyStatus['existsLocal']) { this.setState({showImportPrivateKeyModal: true}); } else { console.log('PGP key exists locally'); } } else { console.log('PGP key was not checked locally'); } } else { console.log('PGP key was not checked on server'); } } } async hideExportPrivateKeyModal() { this.setState({privateKey: null, showExportPrivateKeyModal: false}); } async showExportPrivateKeyModal() { this.setState({showExportPrivateKeyModal: true}); } togglePinned() { console.log('togglePinned', this.state.selectedContact); if (this.state.selectedContact) { - this.getMessages(this.state.selectedContact.uri, !this.state.pinned); + //this.getMessages(this.state.selectedContact.uri, {pinned: !this.state.pinned}); this.setState({pinned: !this.state.pinned}); } } toggleSSI() { // user setting to enable/disable ssiAgent let ssiRequired = !this.state.ssiRequired; console.log('toggleSSI to', ssiRequired); this.setState({ssiRequired: ssiRequired}); if (ssiRequired) { this.initSSIAgent(); } else { this.setState({ssiAgent: null}); } storage.set('ssi', {required: ssiRequired}); } toggleSpeakerPhone() { if (this.state.speakerPhoneEnabled === true) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } toggleCallMeMaybeModal() { this.setState({showCallMeMaybeModal: !this.state.showCallMeMaybeModal}); } toggleQRCodeScanner() { //utils.timestampedLog('Toggle QR code scanner'); this.setState({showQRCodeScanner: !this.state.showQRCodeScanner}); } async handleSSIEnrolment(url) { utils.timestampedLog('SSI enrolment invitation URL', url); if (!this.ssiAgent) { console.log('No SSI agent available yet for handling enrolment to', url); self.pendingSsiUrl = url; return; } try { const ssiConnectionRecord = await this.ssiAgent.connections.receiveInvitationFromUrl(url); utils.timestampedLog('SSI enrolment requested', ssiConnectionRecord.id); setTimeout(() => { this._notificationCenter.postSystemNotification('SSI enrolment requested'); }, 2000); } catch (error) { utils.timestampedLog('SSI enrolment error', error); setTimeout(() => { this._notificationCenter.postSystemNotification('SSI enrolment ' + error); }, 2000); } } speakerphoneOn() { utils.timestampedLog('Speakerphone On'); this.setState({speakerPhoneEnabled: true}); InCallManager.setForceSpeakerphoneOn(true); let call = this.state.currentCall || this.state.incomingCall; if (call) { RNCallKeep.toggleAudioRouteSpeaker(call.id, true); } } speakerphoneOff() { utils.timestampedLog('Speakerphone Off'); this.setState({speakerPhoneEnabled: false}); InCallManager.setForceSpeakerphoneOn(false); let call = this.state.currentCall || this.state.incomingCall; if (call) { RNCallKeep.toggleAudioRouteSpeaker(call.id, false); } } startGuestConference(targetUri) { this.setState({targetUri: targetUri}); this.getLocalMedia({audio: true, video: true}); } outgoingCall(call) { // called by sylkrtc.js when an outgoing call starts call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); this.callKeeper.startOutgoingCall(call); this.updateLoading(null, 'outgoing_call'); } outgoingConference(call) { // called by sylrtc.js when an outgoing conference starts call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); this.callKeeper.startOutgoingCall(call); this.updateLoading(null, 'outgoing_call'); } _onLocalNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); utils.timestampedLog('Handle local iOS PUSH notification: ', notificationContent); } _onNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); const event = notificationContent['event']; const callUUID = notificationContent['session-id']; const to = notificationContent['to_uri']; const from = notificationContent['from_uri']; const displayName = notificationContent['from_display_name']; const outgoingMedia = {audio: true, video: notificationContent['media-type'] === 'video'}; const mediaType = notificationContent['media-type'] || 'audio'; /* * Local Notification Payload * * - `alertBody` : The message displayed in the notification alert. * - `alertAction` : The "action" displayed beneath an actionable notification. Defaults to "view"; * - `soundName` : The sound played when the notification is fired (optional). * - `category` : The category of this notification, required for actionable notifications (optional). * - `userInfo` : An optional object containing additional notification data. */ if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); this.startedByPush = true; this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); this.startedByPush = true; this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'cancel') { utils.timestampedLog('Push notification: cancel call', callUUID); VoipPushNotification.presentLocalNotification({alertBody:'Call cancelled'}); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); this.resetStartedByPush('cancel'); } else if (event === 'message') { utils.timestampedLog('Push for messages received'); VoipPushNotification.presentLocalNotification({alertBody:'Messages received'}); } /* if (notificationContent['event'] === 'incoming_session') { VoipPushNotification.presentLocalNotification({ alertBody:'Incoming ' + notificationContent['media-type'] + ' call from ' + notificationContent['from_display_name'] }); } */ if (VoipPushNotification.wakeupByPush) { utils.timestampedLog('We wake up by push notification'); VoipPushNotification.wakeupByPush = false; VoipPushNotification.onVoipNotificationCompleted(callUUID); } } backToForeground() { //console.log('backToForeground...'); if (this.state.appState !== 'active') { this.callKeeper.backToForeground(); } if (this.state.accountId) { this.handleRegistration(this.state.accountId, this.state.password); } PushNotification.popInitialNotification((notification) => { if (notification) { console.log('Initial push notification', notification); } }); } incomingConference(callUUID, to, from, displayName, outgoingMedia={audio: true, video: true}) { if (this.unmounted) { return; } const mediaType = outgoingMedia.video ? 'video' : 'audio'; utils.timestampedLog('Incoming', mediaType, 'conference invite from', from, displayName, 'to room', to); if (this.state.account && from === this.state.account.id) { utils.timestampedLog('Reject conference call from myself', callUUID); this.callKeeper.rejectCall(callUUID); return; } if (this.autoRejectIncomingCall(callUUID, from, to)) { return; } let incomingContact = this.newContact(from, displayName); this.setState({incomingCallUUID: callUUID, incomingContact: incomingContact}); this.callKeeper.handleConference(callUUID, to, from, displayName, mediaType, outgoingMedia); } startConference(targetUri, options={audio: true, video: true, participants: []}) { this.backToForeground(); this.updateLoading(null, 'start_conference'); utils.timestampedLog('New outgoing conference to room', targetUri); this.setState({targetUri: targetUri}); this.getLocalMedia({audio: options.audio, video: options.video}, '/conference'); this.getMessages(targetUri); } escalateToConference(participants) { let outgoingMedia = {audio: true, video: true}; let mediaType = 'video'; let call; this.setState({selectedContacts: []}); if (this.state.currentCall) { call = this.state.currentCall; } else if (this.state.incomingCall) { call = this.state.currentCall; } else { console.log('No call to escalate'); return } const localStreams = call.getLocalStreams(); if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; if (localStream.getVideoTracks().length == 0) { outgoingMedia.video = false; mediaType = 'audio'; } } this.outgoingMedia = outgoingMedia; this.participantsToInvite = participants; console.log('Escalate', mediaType, 'call', call.id, 'to conference with', participants.toString()); this.hangupCall(call.id, 'escalate_to_conference'); } conferenceInviteFromWebSocket(data) { // comes from web socket utils.timestampedLog('Conference invite from websocket', data.id, 'from', data.originator, 'for room', data.room); if (this.isConference()) { return; } //this._notificationCenter.postSystemNotification('Expecting conference invite', {body: `from ${data.originator.displayName || data.originator.uri}`}); } updateLinkingURL = (event) => { // this handles the use case where the app is running in the background and is activated by the listener... console.log('Updated Linking url', event.url); this.eventFromUrl(event.url); DeepLinking.evaluateUrl(event.url); } eventFromUrl(url) { //console.log('Event from url', url); url = decodeURI(url); try { let direction; let event; let callUUID; let from; let to; let displayName; let mediaType = 'audio'; var url_parts = url.split("/"); let scheme = url_parts[0]; //console.log(url_parts); if (scheme === 'sylk:') { //sylk://conference/incoming/callUUID/from/to/media - when Android is asleep //sylk://call/outgoing/callUUID/to/displayName - from system dialer/history //sylk://call/incoming/callUUID/from/to/displayName/media - when Android is asleep //sylk://call/cancel//callUUID - when Android is asleep event = url_parts[2]; direction = url_parts[3]; callUUID = url_parts[4]; from = url_parts[5]; to = url_parts[6]; displayName = url_parts[7]; mediaType = url_parts[8] || 'audio'; if (event !== 'cancel' && from && from.search('@videoconference.') > -1) { event = 'conference'; to = from; } this.setState({targetUri: from}); } else if (scheme === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link // This URLs are used to request SSI credentials: // must be updated inside: // * ReadyBox as well // * android/app/src/main/AndroidManifest.xml // * ios/sylk/sylk.entitlements if (url.startsWith('https://didcomm.issuer.bloqzone.com/?c_i=')) { this.handleSSIEnrolment(url); } if (url.startsWith('https://ssimandate.vismaconnect.nl/api/acapy?c_i=')) { this.handleSSIEnrolment(url); } direction = 'outgoing'; event = url_parts[3]; if (!event) { return; } to = url_parts[4]; if (!to) { return; } callUUID = uuid.v4(); if (to.indexOf('@') === -1 && event === 'conference') { to = url_parts[4] + '@' + config.defaultConferenceDomain; } else if (to.indexOf('@') === -1 && event === 'call') { to = url_parts[4] + '@' + this.state.defaultDomain; } this.setState({targetUri: to}); } let data = {}; data['session-id'] = callUUID; data['event'] = event; data['to_uri'] = to; data['from_uri'] = from; data['from_display_name'] = displayName; data['media-type'] = mediaType; if (event === 'conference') { utils.timestampedLog('Conference from external URL:', url); this.startedByPush = true; if (direction === 'outgoing' && to) { utils.timestampedLog('Outgoing conference to', to); this.backToForeground(); this.callKeepStartConference(to, {audio: true, video: true, callUUID: callUUID}); } else if (direction === 'incoming' && from) { utils.timestampedLog('Incoming conference from', from); // allow app to wake up this.backToForeground(); const media = {audio: true, video: mediaType === 'video'} this.postAndroidIncomingCallNotification(data); this.incomingConference(callUUID, to, from, displayName, media); } } else if (event === 'call') { this.startedByPush = true; if (direction === 'outgoing') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Outgoing call to', from); this.backToForeground(); this.callKeepStartCall(from, {audio: true, video: false, notification: callUUID}); } else if (direction === 'incoming') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Incoming', mediaType, 'call from', from); //this.playIncomingRingtone(callUUID, true); this.postAndroidIncomingCallNotification(data); this.incomingCallFromPush(callUUID, from, displayName, mediaType, true); } else if (direction === 'cancel') { this.cancelIncomingCall(callUUID); } } else { utils.timestampedLog('Error: Invalid external URL event', event); } } catch (err) { utils.timestampedLog('Error parsing URL', url, ":", err); } } autoRejectIncomingCall(callUUID, from, to) { //utils.timestampedLog('Check auto reject call from', from); if (this.state.blockedUris) { if (this.state.blockedUris.indexOf(from) > -1 || (this.state.blockedUris.indexOf('anonymous@anonymous.invalid') > -1 && (from === 'anonymous@anonymous.invalid' || from.indexOf('@guest.') > -1))) { utils.timestampedLog('Reject call', callUUID, 'from blocked URI', from); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from ${from}`}); return true; } } const fromDomain = '@' + from.split('@')[1] if (this.state.blockedUris && this.state.blockedUris.indexOf(fromDomain) > -1) { utils.timestampedLog('Reject call', callUUID, 'from blocked domain', fromDomain); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from domain ${fromDomain}`}); return true; } if (this.state.currentCall && this.state.incomingCall && this.state.currentCall === this.state.incomingCall && this.state.incomingCall.id !== callUUID) { utils.timestampedLog('Reject second incoming call'); this.callKeeper.rejectCall(callUUID); } if (this.state.account && from === this.state.account.id && this.state.currentCall && this.state.currentCall.remoteIdentity.uri === from) { utils.timestampedLog('Reject call to myself', callUUID); this.callKeeper.rejectCall(callUUID); return true; } if (this._terminatedCalls.has(callUUID)) { utils.timestampedLog('Reject call already terminated', callUUID); this.cancelIncomingCall(callUUID); return true; } if (this.isConference()) { utils.timestampedLog('Reject call while in a conference', callUUID); if (to !== this.state.targetUri) { this._notificationCenter.postSystemNotification('Missed call from', {body: from}); } this.callKeeper.rejectCall(callUUID); return true; } if (this.state.currentCall && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri !== from) { utils.timestampedLog('Reject call while outgoing in progress', callUUID); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Missed call from', {body: from}); return true; } return false; } autoAcceptIncomingCall(callUUID, from) { // TODO: handle ping pong where we call each other back if (this.state.currentCall && this.state.currentCall.direction === 'outgoing' && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri === from) { this.hangupCall(this.state.currentCall.id, 'accept_new_call'); this.setState({currentCall: null}); utils.timestampedLog('Auto accept incoming call from same address I am calling', callUUID); return true; } return false; } incomingCallFromPush(callUUID, from, displayName, mediaType, force) { //utils.timestampedLog('Handle incoming PUSH call', callUUID, 'from', from, '(', displayName, ')'); if (this.unmounted) { return; } if (this.autoRejectIncomingCall(callUUID, from)) { return; } if (this.autoAcceptIncomingCall(callUUID, from)) { return; } this.backToForeground(); this.goToReadyNowAndCancelTimer(); this.setState({targetUri: from}); let skipNativePanel = false; if (!this.callKeeper._calls.get(callUUID) || (this.state.currentCall && this.state.currentCall.direction === 'outgoing')) { //this._notificationCenter.postSystemNotification('Incoming call', {body: `from ${from}`}); if (Platform.OS === 'android' && this.state.appState === 'foreground') { skipNativePanel = true; } } this.callKeeper.incomingCallFromPush(callUUID, from, displayName, mediaType, force, skipNativePanel); } incomingCallFromWebSocket(call, mediaTypes) { if (this.unmounted) { return; } if (this.timeoutIncomingTimer) { console.log('Clear incoming timer'); clearTimeout(this.timeoutIncomingTimer); this.timeoutIncomingTimer = null; } this.callKeeper.addWebsocketCall(call); const callUUID = call.id; const from = call.remoteIdentity.uri; //this.playIncomingRingtone(callUUID); //utils.timestampedLog('Handle incoming web socket call', callUUID, 'from', from, 'on connection', Object.id(this.state.connection)); // because of limitation in Sofia stack, we cannot have more then two calls at a time // we can have one outgoing call and one incoming call but not two incoming calls // we cannot have two incoming calls, second one is automatically rejected by sylkrtc.js if (this.autoRejectIncomingCall(callUUID, from)) { return; } const autoAccept = this.autoAcceptIncomingCall(callUUID, from); this.goToReadyNowAndCancelTimer(); call.mediaTypes = mediaTypes; call.on('stateChanged', this.callStateChanged); this.setState({incomingCall: call}); let skipNativePanel = false; if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { if (Platform.OS === 'android') { this.showAlertPanel(call); skipNativePanel = true; } } console.log('Incoming call extra headers', call.headers); this.callKeeper.incomingCallFromWebSocket(call, autoAccept, skipNativePanel); } missedCall(data) { utils.timestampedLog('Missed call from ' + data.originator.uri, '(', data.originator.displayName, ')'); /* let msg; let current_datetime = new Date(); let formatted_date = utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); msg = formatted_date + " - missed call"; this.saveSystemMessage(data.originator.uri.toLowerCase(), msg, 'incoming', true); */ if (!this.state.currentCall) { let from = data.originator.displayName || data.originator.uri; this._notificationCenter.postSystemNotification('Missed call', {body: `from ${from}`}); if (Platform.OS === 'ios') { VoipPushNotification.presentLocalNotification({alertBody:'Missed call from ' + from}); } } this.updateServerHistory('missedCall') } updateServerHistory(from) { //console.log('updateServerHistory by', from); //this.contactsCount(); if (this.state.serverHistoryUpdatedBy === 'registered' && from === 'syncConversations') { // Avoid double query at start return; } if (this.state.serverHistoryUpdatedBy === 'syncConversations' && from === 'registered') { // Avoid double query at start return; } this.setState({serverHistoryUpdatedBy: from}) if (!this.state.contactsLoaded) { return; } if (!this.state.firstSyncDone) { return; } if (this.currentRoute === '/ready') { this.setState({refreshHistory: !this.state.refreshHistory}); } } startPreview() { this.getLocalMedia({audio: true, video: true}, '/preview'); } sendPublicKey(uri) { if (!uri) { console.log('Missing uri, cannot send public key'); } if (uri === this.state.accountId) { return; } // Send outgoing messages if (this.state.account && this.state.keys && this.state.keys.public) { console.log('Sending public key to', uri); this.state.account.sendMessage(uri, this.state.keys.public, 'text/pgp-public-key'); } else { console.log('No public key available'); } } async saveOutgoingRawMessage(id, from_uri, to_uri, content, contentType) { let timestamp = new Date(); let params; let unix_timestamp = Math.floor(timestamp / 1000); params = [this.state.accountId, id, JSON.stringify(timestamp), unix_timestamp, content, contentType, from_uri, to_uri, "outgoing", "1"]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { //console.log('SQL insert message OK'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } showCallMeModal() { this.setState({showCallMeMaybeModal: true}); setTimeout(() => { this.hideCallMeModal(); }, 25000); } hideCallMeModal() { this.setState({showCallMeMaybeModal: false}); } async saveSylkContact(uri, contact, origin=null) { if (!contact) { contact = this.newContact(uri); } else { contact = this.sanitizeContact(uri, contact, 'saveSylkContact'); } if (!contact) { return; } //console.log('Save Sylk Contact', uri, contact.name, 'by', origin); if (this.sql_contacts_keys.indexOf(uri) > -1) { this.updateSylkContact(uri, contact, origin); return; } let unread_messages = contact.unread.toString(); if (origin === 'saveIncomingMessage' && this.state.selectedContact && this.state.selectedContact.uri === uri) { unread_messages = ''; console.log('Do not update unread messages for', uri); } let conference = contact.conference ? 1: 0; let tags = contact.tags.toString(); let media = contact.lastCallMedia.toString(); let participants = contact.participants.toString(); let unixTime = Math.floor(contact.timestamp / 1000); let params = [this.state.accountId, contact.email, contact.photo, unixTime, uri, contact.name || '', contact.organization || '', unread_messages || '', tags || '', participants || '', contact.publicKey || '', contact.direction, media, conference, contact.lastCallId, contact.lastCallDuration]; await this.ExecuteQuery("INSERT INTO contacts (account, email, photo, timestamp, uri, name, organization, unread_messages, tags, participants, public_key, direction, last_call_media, conference, last_call_id, last_call_duration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { if (result.rowsAffected === 1) { //console.log('SQL inserted contact', contact.uri, 'by', origin); } this.sql_contacts_keys.push(uri); let myContacts = this.state.myContacts; if (uri !== this.state.accountId) { myContacts[uri] = contact; let favorite = myContacts[uri].tags.indexOf('favorite') > -1 ? true: false; let blocked = myContacts[uri].tags.indexOf('blocked') > -1 ? true: false; this.updateFavorite(uri, favorite); this.updateBlocked(uri, blocked); this.setState({myContacts: myContacts}); } else { this.setState({email: contact.email, displayName: contact.name}) if (myContacts[uri].tags.indexOf('chat') > -1 || myContacts[uri].tags.indexOf('history') > -1) { myContacts[uri] = contact; this.setState({myContacts: myContacts}); } } }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') > -1) { //console.log('SQL insert contact failed, try update', uri); this.updateSylkContact(uri, contact, origin); } else { //console.log('SQL insert contact', uri, 'error:', error); //console.log('Existing keys during insert:', this.sql_contacts_keys); } }); } async updateSylkContact(uri, contact, origin=null) { //console.log('updateSylkContact', contact); let unixTime = Math.floor(contact.timestamp / 1000); let unread_messages = contact.unread.toString(); let media = contact.lastCallMedia.toString(); let tags = contact.tags.toString(); let conference = contact.conference ? 1: 0; let participants = contact.participants.toString(); let params = [contact.photo, contact.email, contact.lastMessage, contact.lastMessageId, unixTime, contact.name || '', contact.organization || '', unread_messages || '', contact.publicKey || '', tags, participants, contact.direction, media, conference, contact.lastCallId, contact.lastCallDuration, contact.uri, this.state.accountId]; await this.ExecuteQuery("UPDATE contacts set photo = ?, email = ?, last_message = ?, last_message_id = ?, timestamp = ?, name = ?, organization = ?, unread_messages = ?, public_key = ?, tags = ? , participants = ?, direction = ?, last_call_media = ?, conference = ?, last_call_id = ?, last_call_duration = ? where uri = ? and account = ?", params).then((result) => { if (result.rowsAffected === 1) { //console.log('SQL updated contact', contact.uri, 'by', origin); } let myContacts = this.state.myContacts; if (uri !== this.state.accountId) { myContacts[uri] = contact; let favorite = myContacts[uri].tags.indexOf('favorite') > -1 ? true: false; let blocked = myContacts[uri].tags.indexOf('blocked') > -1 ? true: false; this.updateFavorite(uri, favorite); this.updateBlocked(uri, blocked); this.setState({myContacts: myContacts}); } else { this.setState({email: contact.email, displayName: contact.name}) if (myContacts[uri].tags.indexOf('chat') > -1 || myContacts[uri].tags.indexOf('history') > -1) { myContacts[uri] = contact; this.setState({myContacts: myContacts}); } } }).catch((error) => { console.log('SQL update contact', uri, 'error:', error); }); } async deleteSylkContact(uri) { if (uri === this.state.accountId) { await this.ExecuteQuery("UPDATE contacts set direction = null, last_message = null, last_message_id = null, unread_messages = '' where account = ? and uri = ?", [uri, uri]).then((result) => { console.log('SQL update my own contact'); let myContacts = this.state.myContacts; if (uri in myContacts) { delete myContacts[uri]; this.setState({myContacts: myContacts}); } }).catch((error) => { console.log('Delete update mysql SQL error:', error); }); } else { await this.ExecuteQuery("DELETE from contacts where uri = ? and account = ?", [uri, this.state.accountId]).then((result) => { if (result.rowsAffected > 0) { console.log('SQL deleted contact', uri); } let myInvitedParties = this.state.myInvitedParties; if (uri in myInvitedParties) { delete myInvitedParties[uri]; this.setState({myInvitedParties: myInvitedParties}); } let idx = this.sql_contacts_keys.indexOf(uri); if (idx > -1) { this.sql_contacts_keys.splice(idx, 1); } //console.log('new keys after delete', this.sql_contacts_keys); let myContacts = this.state.myContacts; if (uri in myContacts) { delete myContacts[uri]; this.setState({myContacts: myContacts}); } }).catch((error) => { console.log('Delete contact SQL error:', error); }); } } async replicatePrivateKey(password) { if (!this.state.account) { console.log('No account'); return; } if (!this.state.keys || !this.state.keys.private) { return; } password = password.trim(); const public_key = this.state.keys.public.replace(/\r/g, '').trim(); await OpenPGP.encryptSymmetric(this.state.keys.private, password, KeyOptions).then((encryptedBuffer) => { utils.timestampedLog('Sending encrypted private key'); encryptedBuffer = public_key + "\n" + encryptedBuffer; this.state.account.sendMessage(this.state.account.id, encryptedBuffer, 'text/pgp-private-key'); }).catch((error) => { console.log('Error encrypting private key:', error); }); } processRemotePrivateKey(keyPair) { let regexp; let match; let public_key; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { public_key = match[0]; } if (public_key && this.state.keys && this.state.keys.public === public_key) { console.log('Private key is the same'); return; } this.setState({showImportPrivateKeyModal: true, privateKey: keyPair}); } async savePrivateKey(password) { utils.timestampedLog('Save encrypted private key'); password = password.trim(); let regexp; let match; let keyPair; let public_key; let encrypted_key; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = this.state.privateKey.match(regexp); if (match && match.length === 1) { public_key = match[0]; } if (public_key) { if (this.state.keys && this.state.keys.public === public_key) { this.setState({privateKeyImportStatus: 'Private key is the same', privateKeyImportSuccess: true}); return; } regexp = /(-----BEGIN PGP MESSAGE-----[^]*-----END PGP MESSAGE-----)/ig; match = this.state.privateKey.match(regexp); if (match && match.length === 1) { encrypted_key = match[0]; } if (encrypted_key) { await OpenPGP.decryptSymmetric(encrypted_key, password).then((privateKey) => { utils.timestampedLog('Decrypted PGP private pair'); this.setState({keyDifferentOnServer: false}) keyPair = public_key + "\n" + privateKey; this.processPrivateKey(keyPair); }).catch((error) => { this.setState({privateKeyImportStatus: 'No key received'}); console.log('Error decrypting PGP private key:', error); return }); } else { this.setState({privateKeyImportStatus: 'No encrypted key found'}); console.log('Error parsing PGP private key:', error); return } } else { await OpenPGP.decryptSymmetric(this.state.privateKey, password).then((keyPair) => { utils.timestampedLog('Decrypted PGP private pair'); this.setState({keyDifferentOnServer: false}) this.processPrivateKey(keyPair); }).catch((error) => { this.setState({privateKeyImportStatus: 'No key received'}); console.log('Error decrypting PGP private key:', error); return }); } } async processPrivateKey(keyPair) { utils.timestampedLog('Process key'); keyPair = keyPair.replace(/\r/g, '').trim(); let public_key; let private_key; let status; let keys = this.state.keys || {}; let regexp; let match; regexp = /(-----BEGIN PGP PUBLIC KEY BLOCK-----[^]*-----END PGP PUBLIC KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { public_key = match[0]; } regexp = /(-----BEGIN PGP PRIVATE KEY BLOCK-----[^]*-----END PGP PRIVATE KEY BLOCK-----)/ig; match = keyPair.match(regexp); if (match && match.length === 1) { private_key = match[0]; } if (public_key && private_key) { if (keys.private !== private_key && keys.public !== public_key) { let new_keys = {private: private_key, public: public_key} this.saveMyKey(new_keys); status = 'Private key copied successfully'; if (this.state.account) { this.state.account.sendMessage(this.state.accountId, 'Private key imported on another device', 'text/pgp-public-key-imported'); } this.requestSyncConversations(); } else { status = 'Private key is the same'; console.log(status); } this.setState({privateKeyImportStatus: status, privateKeyImportSuccess: true}); } else { this.setState({privateKeyImportStatus: 'Incorrect password!', privateKeyImportSuccess: false}); } } resetStartedByPush(from) { console.log('resetStartedByPush', from); this.startedByPush = false; if (this.state.lastSyncId) { this.requestSyncConversations(this.state.lastSyncId); } } requestSyncConversations(lastId=null) { console.log('Request sync conversations from', lastId); if (!this.state.account) { return; } if (!this.state.keys) { console.log('Wait for sync until we have keys') return; } if (this.startedByPush) { console.log('Wait for sync until incoming call ends') return; } if (this.syncRequested) { console.log('Sync already requested') return; } this.syncRequested = true; console.log('Request sync messages from server', lastId); this.state.account.syncConversations(lastId); } async savePublicKey(uri, key) { if (uri === this.state.accountId) { return; } if (!key) { console.log('Missing key'); return; } - key = key.replace(/\r/g, '').trim(); if (!key.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the start of PGP public key'); return; } if (!key.endsWith("-----END PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the end of PGP public key'); return; } let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = {}; } if (myContacts[uri].publicKey === key) { console.log('Public key of', uri, 'did not change'); return; } utils.timestampedLog('Public key of', uri, 'saved'); this.saveSystemMessage(uri, 'Public key received', 'incoming'); myContacts[uri].publicKey = key; this.saveSylkContact(uri, myContacts[uri], 'savePublicKey'); this.sendPublicKey(uri); } async savePublicKeySync(uri, key) { console.log('Sync public key from', uri); if (!key) { console.log('Missing key'); return; } key = key.replace(/\r/g, '').trim(); if (!key.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the start of PGP public key'); return; } if (!key.endsWith("-----END PGP PUBLIC KEY BLOCK-----")) { console.log('Cannot find the end of PGP public key'); return; } let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = {}; } if (myContacts[uri].publicKey === key) { console.log('Public key of', uri, 'did not change'); return; } console.log('Public key of', uri, 'saved'); myContacts[uri].publicKey = key; this.saveSylkContact(uri, myContacts[uri], 'savePublicKeySync'); } sendConferenceMessage(message) { if (!this.state.currentCall) { return; } if (!this.isConference(this.state.currentCall)) { return; } this.state.currentCall.sendMessage(message.text, 'text/plain'); message.direction = 'outgoing'; message.sent = true; message.received = true; this.saveConferenceMessage(this.state.currentCall.remoteIdentity.uri, message); } _sendMessage(uri, text, id, contentType, timestamp) { // Send outgoing messages if (this.state.account) { //console.log('Send', contentType, 'message', id, 'to', uri); let message = this.state.account.sendMessage(uri, text, contentType, {id: id, timestamp: timestamp}, (error) => { if (error) { console.log('Message', id, 'sending error:', error); this.outgoingMessageStateChanged(id, 'failed'); let status = error.toString(); if (status.indexOf('DNS lookup error') > -1) { status = 'Domain not found'; } this.renderSystemMessage(uri, status, 'incoming'); } }); //console.log(message); //message.on('stateChanged', (oldState, newState) => {this.outgoingMessageStateChanged(message.id, oldState, newState)}) } } textToGiftedMessage(text) { return { _id: uuid.v4(), text: text, createdAt: new Date(), received: false, direction: 'outgoing', user: {} }; } - async sendMessage(uri, message) { + async sendMessage(uri, message, contentType='text/plain') { message.sent = false; message.received = false; message.pending = true; message.direction = 'outgoing'; + //console.log('----sendMessage', uri, message); + let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) === -1) { renderMessages[uri] = []; } let public_keys; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys) { public_keys = this.state.keys.public + "\n" + this.state.myContacts[uri].publicKey; } - if (!message.contentType) { - message.contentType = 'text/plain'; + message.contentType = contentType; + message.content = message.text + message.content_type = contentType; + + if (contentType === 'application/sylk-file-transfer') { + let file_transfer = message.metadata; + const localPath = RNFS.DocumentDirectoryPath + "/" + file_transfer.sender.uri + "/" + file_transfer.receiver.uri + "/" + file_transfer.transfer_id + "/" + file_transfer.filename; + const dirname = path.dirname(localPath); + await RNFS.mkdir(dirname); + await RNFS.copyFile(file_transfer.path, localPath); + file_transfer.local_url = localPath; + file_transfer.url = this.state.fileTransferUrl + '/' + file_transfer.sender.uri + '/' + file_transfer.receiver.uri + '/' + file_transfer.transfer_id + '/' + file_transfer.filename; + message.metadata = file_transfer; + this.uploadFile(message.metadata); } - if (message.contentType !== 'text/pgp-public-key' && public_keys && this.state.keys) { + if (message.contentType !== 'application/sylk-file-transfer' && message.contentType !== 'text/pgp-public-key' && public_keys && this.state.keys) { await OpenPGP.encrypt(message.text, public_keys).then((encryptedMessage) => { this._sendMessage(uri, encryptedMessage, message._id, message.contentType, message.createdAt); 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); + this.saveOutgoingMessage(uri, message, 0, message.contentType); + if (message.contentType !== 'application/sylk-file-transfer' ) { + this._sendMessage(uri, message.text, message._id, message.contentType, message.createdAt); + } } - renderMessages[uri].push(message); - if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; - selectedContact.lastMessage = message.text.substring(0, 100); + selectedContact.lastMessage = this.buildLastMessage(message) selectedContact.timestamp = message.createdAt; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; - this.setState({selectedContact: selectedContact, messages: renderMessages}); - } else { - this.setState({messages: renderMessages}); + this.setState({selectedContact: selectedContact}); + } + + console.log('Added render message', message._id, message.contentType); + renderMessages[uri].push(message); + this.setState({messages: renderMessages}); + } + + async uploadFile(file_transfer){ + let encrypted_file; + let local_url = file_transfer.local_url; + let remote_url = file_transfer.url; + let uri = file_transfer.receiver.uri; + let outputFile; + let must_encrypt = false; + + if (!file_transfer.filetype) { + let type = await fileType(file_transfer.local_url); + file_transfer.filetype = type ? type.mime : 'application/octet-stream'; + } + + if (!this.state.connection) { + console.log('Wait for Internet connection...'); + return; + } + + if (this.state.connection.state !== 'ready') { + console.log('Wait for Internet connection...'); + return; + } + + if (remote_url in this.uploadRequests) { + console.log('Upload already in progres', file_transfer.url); + return; + } + + this.uploadRequests[remote_url] = file_transfer; + + if (!local_url && file_transfer.transfer_id) { + this.deleteMessage(file_transfer.transfer_id, uri); + return; + } + + if (must_encrypt) { + if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys.public) { + let encrypted_file = file_transfer.local_url + ".asc"; + console.log('Encrypting file for', uri); + let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; + let original_content = await RNFS.readFile(local_url, 'base64'); + console.log('Base64 file read', local_url, original_content.length, 'bytes'); + let encryptedMessage = await OpenPGP.encrypt(original_content, public_keys); + + await RNFS.writeFile(encrypted_file, encryptedMessage, 'utf8'); + let check_file = await RNFS.readFile(encrypted_file, 'utf8'); + console.log('Content encrypted', check_file.substring(0, 80), check_file.slice(-80)); + + /* + let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; + outputFile = await OpenPGP.encryptFile(file_transfer.local_url, encrypted_file, public_keys); + if (outputFile) { + local_url = encrypted_file; + RNFS.readFile(encrypted_file, 'base64').then((content) => { + console.log('Encrypted data', content); + }).catch((error) => { + console.log('Reading data for', file_transfer.filename, 'error:', error); + }); + remote_url = remote_url + '.asc'; + } + */ + + } else { + console.log('No public key available for', uri); + } + } + + console.log('--- Upload', local_url); + const xhr = new XMLHttpRequest(); + + xhr.onload = () => { + if (xhr.status === 200) { + console.log('File uploaded:', local_url); + } else { + delete this.uploadRequests[remote_url]; + const error = new Error(xhr.response); + console.log(error); + this.outgoingMessageStateChanged(file_transfer.transfer_id, 'failed'); + } + this.updateRenderFileTransferBubble(file_transfer); + delete this.uploadRequests[remote_url] + }; + + xhr.open('POST', remote_url); + xhr.setRequestHeader('content-type', file_transfer.filetype); + this.updateRenderFileTransferBubble(file_transfer, 'Uploading file...'); + xhr.send({ uri: 'file://'+ local_url }); + if (xhr.upload) { + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + // evt.loaded the bytes the browser received + // evt.total the total bytes set by the header + var progress = Math.floor((event.loaded/event.total) * 100); + console.log('Upload ' + progress + '%!'); + file_transfer.progress = progress; + this.updateRenderFileTransferBubble(file_transfer, 'Uploaded ' + progress + '%'); + } + }; } } async reSendMessage(message, uri) { await this.deleteMessage(message._id, uri).then((result) => { message._id = uuid.v4(); this.sendMessage(uri, message); }).catch((error) => { console.log('Failed to delete old messages'); }); } - async saveOutgoingMessage(uri, message, encrypted=0) { - this.saveOutgoingChatUri(uri, message.text); - //console.log('saveOutgoingMessage', message.text); + async saveOutgoingMessage(uri, message, encrypted=0, content_type="text/plain") { + //console.log('saveOutgoingMessage', message._id, content_type, message.metadata); + if (content_type !== 'application/sylk-file-transfer') { + this.saveOutgoingChatUri(uri, message); + } + let ts = message.createdAt; let unix_timestamp = Math.floor(ts / 1000); - let params = [this.state.accountId, message._id, JSON.stringify(ts), unix_timestamp, message.text, "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'); + let params = [this.state.accountId, message._id, JSON.stringify(ts), unix_timestamp, message.text, content_type, JSON.stringify(message.metadata), this.state.accountId, uri, "outgoing", "1", encrypted]; + await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, pending, encrypted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { + }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { - console.log('SQL error:', error); + console.log('saveOutgoingMessage SQL error:', error); } }); } async saveConferenceMessage(room, message) { - console.log('Save conference message', message._id); let messages = this.state.messages; let ts = message.createdAt; let unix_timestamp = Math.floor(ts / 1000); - let contentType = "text/plain"; + let contentType = message.metadata && message.metadata.filename ? "application/sylk-file-transfer" : "text/plain"; if (!message.direction) { message.direction = message.received ? 'incoming' : 'outgoing'; } - let from_uri = message.direction === 'incoming' || message.received ? room : this.state.accountId; - let to_uri = message.direction === 'incoming' || message.received ? this.state.accountId : room; + let from_uri = message.direction === 'incoming' ? room : this.state.accountId; + let to_uri = message.direction === 'incoming' ? this.state.accountId : room; let system = message.system ? '1' : null; let sender = !system ? message.user._id : null; var content = message.text; var params = [this.state.accountId, system, JSON.stringify(message.metadata), message.image, sender, message.local_url, message.url, message._id, JSON.stringify(ts), unix_timestamp, content, contentType, from_uri, to_uri, message.direction, 0, message.sent ? 1: 0, message.received ? 1: 0]; await this.ExecuteQuery("INSERT INTO messages (account, system, metadata, image, sender, local_url, url, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { - console.log('SQL insert conference message', message._id); + console.log('SQL insert conference message', message._id, from_uri, to_uri, message.direction); if (room in messages) { + messages[room].push(message); } else { messages[room] = [message]; } - messages[room].push(message); this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { - console.log('SQL error:', error); + console.log('SQL error:', error.message); } }); } async updateConferenceMessage(room, message, update=false) { - console.log('Update conference message', message._id); + //console.log('Update conference message', message._id, 'for room', room); let messages = this.state.messages; let sent = message.sent ? 1 : 0; let received = message.received ? 1 : 0; - var params = [JSON.stringify(message.metadata), message.image, message.local_url, message.text, 0, sent, received, message._id]; - await this.ExecuteQuery("update messages set metadata = ?, image = ?, local_url = ?, content = ?, pending = ?, sent = ?, received = ? where msg_id = ?", params).then((result) => { + var params = [JSON.stringify(message.metadata), message.text, 0, sent, received, message._id]; + await this.ExecuteQuery("update messages set metadata = ?, content = ?, pending = ?, sent = ?, received = ? where msg_id = ?", params).then((result) => { console.log('SQL update conference message', message._id); - let renderMessages = messages[room]; - let newRenderMessages = []; + }).catch((error) => { + if (error.message.indexOf('UNIQUE constraint failed') === -1) { + console.log('updateConferenceMessage SQL error:', error.message); + } + }); + + let renderMessages = messages[room]; + let newRenderMessages = []; + if (renderMessages) { renderMessages.forEach((msg) => { if (msg._id === message._id) { - msg.local_url = message.local_url; msg.image = message.image; - msg.txt = message.text; + msg.video = message.video; + msg.text = message.text; msg.metadata = message.metadata; msg.pending = message.pending; msg.failed = message.failed; msg.sent = message.sent; msg.received = message.received; } newRenderMessages.push(msg); }); messages[room] = newRenderMessages; this.setState({messages: messages}); - }).catch((error) => { - if (error.message.indexOf('UNIQUE constraint failed') === -1) { - console.log('SQL error:', error); - } - }); + } } async deleteConferenceMessage(room, message) { console.log('Delete conference message', message._id); let messages = this.state.messages; var params = [message._id]; await this.ExecuteQuery("delete from messages where msg_id = ?", params).then((result) => { console.log('SQL delete conference message', message._id); let renderMessages = messages[room]; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id !== message._id) { newRenderMessages.push(msg); } }); messages[room] = newRenderMessages; this.setState({messages: messages}); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } - sql2GiftedChat(item, content) { - let image; - let timestamp = new Date(item.unix_timestamp * 1000); - - let failed = (item.received === 0) ? true: false; - let received = item.received === 1 ? true : false; - let sent = item.sent === 1 ? true : false; - let pending = item.pending === 1 ? true : false; - - //console.log('sql2GiftedChat', failed); - - let msg; - let from_uri = item.sender ? item.sender : item.from_uri; - - if (!item.image && item.local_url && utils.isImage(item.local_url)) { - item.image = item.local_url; - } - - if (!item.video && item.local_url && utils.isVideo(item.local_url)) { - //item.video = item.local_url; - } - - if (!item.audio && item.local_url && utils.isAudio(item.local_url)) { - //item.audio = item.local_url; - } - - let metadata = {}; - - if (item.metadata) { - let metadata_obj = JSON.parse(item.metadata); - Object.assign(metadata, metadata_obj); - } - - msg = { - _id: item.msg_id, - key: item.msg_id, - image: item.image, - video: item.video, - audio: item.audio, - url: item.url, - metadata: metadata, - local_url: item.local_url, - text: content || item.content || 'Empty message', - encrypted: item.encrypted, - createdAt: timestamp, - sent: sent, - direction: item.direction, - received: received, - pending: pending, - system: item.system === 1 ? true : false, - failed: failed, - pinned: (item.pinned === 1) ? true: false, - user: item.direction == 'incoming' ? {_id: from_uri, name: from_uri} : {} - } - //console.log(msg); - return msg; - } - async outgoingMessageStateChanged(id, state) { let query; // mark message status // state can be failed or accepted utils.timestampedLog('Outgoing message', id, 'is', state); if (state === 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state === 'failed') { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); if (query) { await this.ExecuteQuery(query).then((results) => { - this.updateRenderMessage(id, state); + this.updateRenderMessageState(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } } - async updateFileDownload(id, url, filePath) { - let query = "UPDATE messages set local_url = ? where msg_id = ?"; - //console.log(query); - await this.ExecuteQuery(query[filePath, id]).then((results) => { - console.log('URL', url, 'was downloaded to', filePath); + async saveDownloadTask(id, url, local_url) { + //console.log('saveDownloadTask', url, local_url); + let query = "SELECT * from messages where msg_id = '" + id + "';"; + this.ExecuteQuery(query,[]).then((results) => { + let rows = results.rows; + let file_transfer = {}; + if (rows.length === 1) { + var item = rows.item(0); + let metadata = item.metadata || item.content; + try { + file_transfer = JSON.parse(metadata); + } catch (e) { + console.log('Error decoding json in saveDownloadTask', metadata); + return; + } + + let uri = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; + file_transfer.local_url = local_url; + file_transfer.paused = false; + + this.ExecuteQuery("UPDATE messages set metadata = ? where msg_id = ?", [JSON.stringify(file_transfer), id]).then((results) => { + console.log('File transfer updated', id); + if (local_url.endsWith('.asc')) { + this.decryptFile(file_transfer); + } else { + this.updateRenderFileTransferBubble(file_transfer); + } + }).catch((error) => { + console.log('saveDownloadTask update SQL error:', error); + }); + } + }).catch((error) => { - console.log('SQL query:', query); - console.log('SQL error:', error); + console.log('saveDownloadTask select SQL error:', error); }); } async messageStateChanged(id, state, data) { // valid API states: pending -> accepted -> delivered -> displayed, // error, failed or forbidden // valid UI render states: pending, read, received let reason = data.reason; let code = data.code; let failed = state === 'failed'; if (failed && code) { if (code > 500 || code === 408) { utils.timestampedLog('Message', id, 'failed on server:', reason, code); } } utils.timestampedLog('Message', id, 'is', state); let query; const failed_states = ['failed', 'error', 'forbidden']; if (state == 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state == 'delivered') { query = "UPDATE messages set pending = 0, sent = 1 where msg_id = '" + id + "'"; } else if (state == 'displayed') { query = "UPDATE messages set received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed_states.indexOf(state) > -1) { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else { console.log('Invalid message state', id, state); return; } //console.log(query); await this.ExecuteQuery(query).then((results) => { - this.updateRenderMessage(id, state, reason, code); + this.updateRenderMessageState(id, state); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } + async fileTransferStateChanged(id, state, file_transfer) { + let failed = state === 'failed'; + + utils.timestampedLog('File transfer', id, 'is', state); + let query; + + const failed_states = ['failed', 'error', 'forbidden']; + + if (state == 'accepted') { + query = "UPDATE messages set metadata = ?, pending = 0 where msg_id = '" + id + "'"; + } else if (state == 'delivered') { + query = "UPDATE messages set metadata = ?, pending = 0, sent = 1 where msg_id = '" + id + "'"; + } else if (state == 'displayed') { + query = "UPDATE messages set metadata = ?, received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; + } else if (failed_states.indexOf(state) > -1) { + file_transfer.failed = true; + query = "UPDATE messages set metadata = ?, received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; + } else { + console.log('Invalid file transfer state', id, state); + return; + } + + //console.log(query); + await this.ExecuteQuery(query, [JSON.stringify(file_transfer)]).then((results) => { + this.updateRenderFileTransferBubble(file_transfer); + console.log('SQL update OK:', query); + }).catch((error) => { + console.log('fileTransferStateChanged SQL error:', error); + }); + } + messageStateChangedSync(obj) { // valid API states: pending -> accepted -> delivered -> displayed, // error, failed or forbidden // valid UI render states: pending, read, received let id = obj.messageId; let state = obj.state; //console.log('Sync message', id, 'state', state); let query; const failed_states = ['failed', 'error', 'forbidden']; if (state == 'accepted') { query = "UPDATE messages set pending = 0 where msg_id = '" + id + "'"; } else if (state == 'delivered') { query = "UPDATE messages set pending = 0, sent = 1 where msg_id = '" + id + "'"; } else if (state == 'displayed') { query = "UPDATE messages set received = 1, sent = 1, pending = 0 where msg_id = '" + id + "'"; } else if (failed_states.indexOf(state) > -1) { query = "UPDATE messages set received = 0, sent = 1, pending = 0 where msg_id = '" + id + "'"; } //console.log(query); this.ExecuteQuery(query).then((results) => { //console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } - async deleteMessage(id, uri, local=true) { + async deleteMessage(id, uri, remote=true) { utils.timestampedLog('Message', id, 'is deleted'); let query; // TODO send request to server //console.log(query); - if (local) { + this.removeFilesForMessage(id, uri); + if (remote) { this.addJournal(id, 'removeMessage', {uri: uri}); } - this.removeFileFromMessage(id, uri); } async refetchMessages(days=1) { let timestamp = new Date(); let params; let unix_timestamp = Math.floor(timestamp / 1000); unix_timestamp = unix_timestamp - days * 24 * 3600; params = [this.state.accountId, unix_timestamp]; + this.syncRequested = false; this.ExecuteQuery("select * from messages where account = ? and unix_timestamp < ? order by unix_timestamp desc limit 1", params).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); this.ExecuteQuery("delete from messages where account = ? and unix_timestamp > ?", params).then((results) => { - console.log('SQL deleted', results.rowsAffected, 'messages'); + //console.log('SQL deleted', results.rowsAffected, 'messages'); this._notificationCenter.postSystemNotification(results.rowsAffected + ' messages removed'); console.log('Sync conversations since', item.msg_id, new Date(item.unix_timestamp * 1000)); this.setState({saveLastSyncId: item.msg_id}); setTimeout(() => { this.requestSyncConversations(item.msg_id); }, 100); }); } }).catch((error) => { console.log('SQL error:', error); }); } - removeFileFromMessage(id, uri) { + removeFilesForMessage(id, uri) { let query = "SELECT * from messages where msg_id = '" + id + "';"; this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); - if (item.local_url) { - RNFS.unlink(item.local_url).then((success) => { - console.log('Removed file', item.local_url); + if (item.metadata) { + let file_transfer = JSON.parse(item.metadata); + let remote_party = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; + let dir_path = RNFS.DocumentDirectoryPath + "/" + this.state.accountId + "/" + remote_party + "/" + id + "/"; + RNFS.unlink(dir_path).then((success) => { + console.log('Removed directory', dir_path); }).catch((err) => { - console.log('Error deleting file', item.local_url, err.message); + console.log('Error deleting directory', dir_path, err.message); }); - } else if (item.url) { - console.log('No local file available for', item.url); } query = "DELETE from messages where msg_id = '" + id + "'"; this.ExecuteQuery(query).then((results) => { this.deleteRenderMessage(id, uri); - console.log('SQL deleted', results.rowsAffected, 'messages'); + //console.log('SQL deleted', results.rowsAffected, 'messages'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteMessageSync(id, uri) { //console.log('Sync message', id, 'is deleted'); let query; + this.removeFilesForMessage(id, uri); query = "DELETE from messages where msg_id = '" + id + "'"; this.ExecuteQuery(query).then((results) => { this.deleteRenderMessageSync(id, uri); // console.log('SQL update OK'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async expireMessage(id, duration=300) { utils.timestampedLog('Expire message', id, 'in', duration, 'seconds after read'); // TODO expire message } async deleteRenderMessage(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let myContacts = this.state.myContacts; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; if (uri in myContacts) { myContacts[uri].totalMessages = myContacts[uri].totalMessages - 1; if (existingMessages.length > 0 && existingMessages[0].id === id) { myContacts[uri].lastMessage = null; myContacts[uri].lastMessageId = null; } } this.setState({messages: renderedMessages, myContacts: myContacts}); } } async deleteRenderMessageSync(id, uri) { let changes = false; let renderedMessages = this.state.messages; let newRenderedMessages = []; let existingMessages = []; if (uri in this.state.messages) { existingMessages = renderedMessages[uri]; existingMessages.forEach((m) => { if (m._id !== id) { newRenderedMessages.push(m); } else { changes = true; } }); } if (changes) { renderedMessages[uri] = newRenderedMessages; this.setState({messages: renderedMessages}); } let idx = 'remove' + id; this.remove_sync_pending_item(idx); } async sendPendingMessage(uri, text, id, contentType, timestamp) { utils.timestampedLog('Outgoing pending message', id); if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey && this.state.keys.public) { let public_keys = this.state.myContacts[uri].publicKey + "\n" + this.state.keys.public; await OpenPGP.encrypt(text, public_keys).then((encryptedMessage) => { //console.log('Outgoing encrypted message to', uri); this._sendMessage(uri, encryptedMessage, id, contentType, timestamp); }).catch((error) => { console.log('Failed to encrypt message:', error); this.outgoingMessageStateChanged(id, 'failed'); //this.saveSystemMessage(uri, 'Failed to encrypt message', 'outgoing'); }); } else { //console.log('Outgoing non-encrypted message to', uri); this._sendMessage(uri, text, id, contentType, timestamp); } } async sendPendingMessages() { + //console.log('sendPendingMessages'); + if (this.mustLogout) { return; } + let content; - await this.ExecuteQuery("SELECT * from messages where pending = 1 and content_type like 'text/%' and from_uri = ?", [this.state.accountId]).then((results) => { + let metadata; + //await this.ExecuteQuery("SELECT * from messages where pending = 1 and content_type like 'text/%' and from_uri = ?", [this.state.accountId]).then((results) => { + await this.ExecuteQuery("SELECT * from messages where pending = 1 and from_uri = ?", [this.state.accountId]).then((results) => { let rows = results.rows; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); - if (item.to_uri.indexOf('@conference.')) { + if (item.to_uri.indexOf('@conference.') > -1) { + console.log('Skip outgoing conference conference messages'); continue; } - if (item.to_uri.indexOf('@videoconference')) { + if (item.to_uri.indexOf('@videoconference') > -1) { + console.log('Skip outgoing videoconference conference messages'); continue; } - content = item.content; let timestamp = new Date(item.unix_timestamp * 1000); - this.sendPendingMessage(item.to_uri, content, item.msg_id, item.content_type, timestamp); + console.log('Pending outgoing message', item.msg_id, item.content_type, item.to_uri); + if (item.content_type === 'application/sylk-file-transfer') { + try { + metadata = JSON.parse(item.metadata); + if (metadata) { + this.uploadFile(metadata); + } else { + this.deleteMessage(item.msg_id, item.msg_id.to_uri); + } + + } catch (e) { + console.log("Error decoding outgoing file transfer json sql: ", e); + this.deleteMessage(item.msg_id, item.to_uri); + } + } else { + this.sendPendingMessage(item.to_uri, item.content, item.msg_id, item.content_type, timestamp); + } } }).catch((error) => { console.log('SQL error:', error); }); await this.ExecuteQuery("SELECT * FROM messages where direction = 'incoming' and system is null and received = 0 and from_uri = ?", [this.state.accountId]).then((results) => { //console.log('SQL get messages OK'); let rows = results.rows; let imdn_msg; for (let i = 0; i < rows.length; i++) { if (this.mustLogout) { return; } var item = rows.item(i); let timestamp = JSON.parse(item.timestamp, _parseSQLDate); imdn_msg = {id: item.msg_id, timestamp: timestamp, from_uri: item.from_uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { query = "UPDATE messages set received = 1 where msg_id = " + item.msg_id; //console.log(query); this.ExecuteQuery(query).then((results) => { }).catch((error) => { console.log('SQL error:', error); }); } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } - async updateRenderMessage(id, state, reason=null, code=null) { + async updateRenderMessageState(id, state) { let query; let uri; let changes = false; - //console.log('updateRenderMessage', id, state); + //console.log('updateMessage', id, state); query = "SELECT * from messages where msg_id = '" + id + "';"; //console.log(query); await this.ExecuteQuery(query,[]).then((results) => { let rows = results.rows; if (rows.length === 1) { var item = rows.item(0); //console.log(item); uri = item.direction === 'outgoing' ? item.to_uri : item.from_uri; //console.log('Message uri', uri, 'new state', state); if (uri in this.state.messages) { let renderedMessages = this.state.messages; renderedMessages[uri].forEach((m) => { if (m._id === id) { if (state === 'accepted') { m.pending = false; changes = true; } if (state === 'delivered') { m.sent = true; m.pending = false; changes = true; } if (state === 'displayed') { m.received = true; m.sent = true; m.pending = false; changes = true; this.playMessageSound('outgoing'); } if (state === 'failed') { m.received = false; m.sent = false; m.pending = false; m.failed = true; changes = true; } if (state === 'pinned') { m.pinned = true; changes = true; } if (state === 'unpinned') { m.pinned = false; changes = true; } } }); if (changes) { this.setState({messages: renderedMessages}); if (state === 'failed') { - reason = 'Message delivery failed: ' + reason; - if (code) { - reason = reason + '('+ code + ')'; - } - this.renderSystemMessage(uri, reason, 'incoming'); + this.renderSystemMessage(uri, 'Message delivery failed', 'incoming'); } } } } }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } - async saveOutgoingChatUri(uri, content='') { + async saveOutgoingChatUri(uri, message) { //console.log('saveOutgoingChatUri', uri); let query; + let content = message.text; let myContacts = this.state.myContacts; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } this.lookupPublicKey(myContacts[uri]); myContacts[uri].unread = []; if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { - myContacts[uri].lastMessage = content.substring(0, 100); + myContacts[uri].lastMessage = this.buildLastMessage(message); + myContacts[uri].lastMessageId = message.id; } if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } - myContacts[uri].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.updateRenderMessageState(id, 'pinned') this.addJournal(id, 'pinMessage'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } unpinMessage(id) { let query; query = "UPDATE messages set pinned = 0 where msg_id ='" + id + "'"; //console.log(query); this.ExecuteQuery(query).then((results) => { - this.updateRenderMessage(id, 'unpinned') + this.updateRenderMessageState(id, 'unpinned') this.addJournal(id, 'unPinMessage'); console.log('Message', id, 'unpinned'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async addJournal(id, action, data={}) { //console.log('Add journal entry:', action, id); this.mySyncJournal[uuid.v4()] = {id: id, action: action, data: data}; this.replayJournal(); } async replayJournal() { if (!this.state.account) { utils.timestampedLog('Sync journal later when going online...'); return; } if (this.mustLogout) { return; } let op; let executed_ops = []; Object.keys(this.mySyncJournal).forEach((key) => { if (this.mustLogout) { return; } executed_ops.push(key); op = this.mySyncJournal[key]; utils.timestampedLog('Sync journal', op.action, op.id); if (op.action === 'removeConversation') { this.state.account.removeConversation(op.id, (error) => { // TODO: add period and delete remote flags if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation was completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'readConversation') { this.state.account.markConversationRead(op.id, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } else if (op.action === 'removeMessage') { this.state.account.removeMessage({id: op.id, receiver: op.data.uri}, (error) => { if (!error) { //utils.timestampedLog(op.action, op.id, 'journal operation completed'); executed_ops.push(key); } else { utils.timestampedLog(op.action, op.id, 'journal operation failed:', error); } }); } }); executed_ops.forEach((key) => { delete this.mySyncJournal[key]; }); storage.set('mySyncJournal', this.mySyncJournal); this.sendPendingMessages(); } async confirmRead(uri){ if (uri.indexOf('@') === -1) { return; } if (uri.indexOf('@conference.') > -1) { return; } if (uri.indexOf('@videoconference.') > -1) { return; } if (uri in this.state.decryptingMessages) { return; } //console.log('Confirm read messages for', uri); let displayed = []; await this.ExecuteQuery("SELECT * FROM messages where from_uri = '" + uri + "' and received = 1 and encrypted not in (1, 3) and system is NULL and to_uri = ?", [this.state.accountId]).then((results) => { let rows = results.rows; if (rows.length > 0) { //console.log('We must confirm read of', rows.length, 'messages'); } for (let i = 0; i < rows.length; i++) { var item = rows.item(i); if (this.sendDispositionNotification(item)) { displayed.push(item.msg_id); } } if (displayed.length > 0) { let sql_ids = ''; let i = 1; displayed.forEach((msg_id) => { sql_ids = sql_ids + "'" + msg_id + "'"; if (i < displayed.length) { sql_ids = sql_ids + ', '; } i = i + 1; }); let query = "UPDATE messages set received = 2 where msg_id in (" + sql_ids + ")"; //console.log(query); this.ExecuteQuery(query).then((results) => { //console.log('Sent disposition saved for', displayed.length, 'messages'); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } }).catch((error) => { console.log('SQL error:', error); }); this.resetUnreadCount(uri); } async resetUnreadCount(uri) { //console.log('--- resetUnreadCount', uri); let myContacts = this.state.myContacts; let missedCalls = this.state.missedCalls; let idx; let changes = false; if (uri in myContacts) { } else { return; } if (myContacts[uri].unread.length > 0) { myContacts[uri].unread = []; myContacts[uri].unread.forEach((id) => { idx = missedCalls.indexOf(id); if (idx > -1) { missedCalls.splice(idx, 1); } }); changes = true; } if (myContacts[uri].lastCallId) { idx = missedCalls.indexOf(myContacts[uri].lastCallId); if (idx > -1) { missedCalls.splice(idx, 1); } } idx = myContacts[uri].tags.indexOf('missed'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); changes = true; } this.updateTotalUread(myContacts); if (changes) { this.saveSylkContact(uri, myContacts[uri], 'resetUnreadCount'); this.addJournal(uri, 'readConversation'); } this.setState({missedCalls: missedCalls}); } async sendDispositionNotification(message, state='displayed') { if (!this.state.account) { return false; } let query; let result = {}; let id = message.msg_id || message.id; let uri = message.sender ? message.sender.uri : message.from_uri; this.state.account.sendDispositionNotification(uri, id, message.timestamp, state,(error) => { if (!error) { utils.timestampedLog('Message', id, 'was', state, 'now'); return true; } else { utils.timestampedLog(state, 'notification for message', id, 'send failed:', error); return false; } }); return false; } loadEarlierMessages() { if (!this.state.selectedContact) { return; } let myContacts = this.state.myContacts; let uri = this.state.selectedContact.uri; let limit = this.state.messageLimit * this.state.messageZoomFactor; if (myContacts[uri].totalMessages < limit) { //console.log('No more messages for', uri); return; } let messageZoomFactor = this.state.messageZoomFactor; messageZoomFactor = messageZoomFactor + 1; this.setState({messageZoomFactor: messageZoomFactor}); setTimeout(() => { this.getMessages(this.state.selectedContact.uri); }, 10); } + async checkFileTransfer(file_transfer) { + if (file_transfer.local_url) { + const exists = await RNFS.exists(file_transfer.local_url); + if (exists) { + const type = await fileType(file_transfer.local_url); + const { size } = await RNFetchBlob.fs.stat(file_transfer.local_url); + //console.log('File exists local', file_transfer.transfer_id, file_transfer.local_url); + if (size === 0) { + let uri = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; + this.deleteMessage(file_transfer.transfer_id, uri); + } + } + + return; + } + + //console.log('checkFileTransfer', file_transfer); + + let difference; + let now = new Date(); + let until = new Date(file_transfer.until); + + if (now.getTime() > until.getTime()) { + console.log('File transfer expired:', file_transfer.transfer_id, file_transfer.filetype); + return; + } + + if (file_transfer.paused) { + console.log('File transfer is paused', file_transfer.transfer_id, file_transfer.filetype); + return; + } + + if (file_transfer.failed) { + console.log('File transfer is failed', file_transfer.transfer_id, file_transfer.filetype); + return; + } + + let ft_ts = new Date(file_transfer.timestamp); + difference = now.getTime() - ft_ts.getTime(); + let days = Math.ceil(difference / (1000 * 3600 * 24)); + + if (days < 10) { + if (utils.isImage(file_transfer.filename)) { + this.downloadFile(file_transfer); + } else { + if (file_transfer.filesize < 1000 * 1000) { + this.downloadFile(file_transfer); + } + } + } + } + + async downloadFile(file_transfer, force=false) { + const res = await RNFS.getFSInfo(); + console.log('Available space', Math.ceil(res.freeSpace/1024/1024), 'MB'); + + if (res.freeSpace < file_transfer.filesize) { + this._notificationCenter.postSystemNotification('Not enough free space'); + return; + } + + let id = file_transfer.transfer_id; + let remote_party = file_transfer.sender.uri === this.state.accountId ? file_transfer.receiver.uri : file_transfer.sender.uri; + let dir_path = RNFS.DocumentDirectoryPath + "/" + this.state.accountId + "/" + remote_party + "/" + id + "/"; + + if (force) { + RNFS.unlink(dir_path) + .then(() => { + utils.timestampedLog('File transfer directory deleted', dir_path); + }) + // `unlink` will throw an error, if the item to unlink does not exist + .catch((err) => { + console.log(err.message); + }); + + file_transfer.local_url = null; + file_transfer.decryption_failed = false; + file_transfer.failed = false; + if (file_transfer.url.endsWith('.asc') && !file_transfer.filename.endsWith('.asc')) { + file_transfer.filename = file_transfer.filename + ('.asc'); + } + this.updateFileTransferMessageMetadata(file_transfer, 0); + } + + let file_path = dir_path + "/" + file_transfer.filename; + let tmp_file_path = file_path + '.tmp'; + + if (id in this.downloadRequests) { + this.downloadRequests[id].stop(); + console.log('File transfer stopped', id); + file_transfer.paused = true; + this.updateFileTransferMessageMetadata(file_transfer, 0); + delete this.downloadRequests[id]; + return; + } + + console.log('Downloading file', file_transfer.filetype, file_transfer.url); + // add a timer to cancel the download + + file_transfer.paused = false; + this.updateRenderFileTransferBubble(file_transfer, 'Downloading file, press to cancel'); + + this.downloadRequests[id] = RNBackgroundDownloader.download({ + id: id, + url: file_transfer.url, + destination: tmp_file_path, + }).begin((size) => { + console.log('File', file_transfer.filename, 'has', size, 'bytes'); + this.updateRenderFileTransferBubble(file_transfer, 'Downloading ' + utils.beautySize(file_transfer.filesize), ', press to cancel'); + }).progress((percent) => { + const progress = Math.ceil(percent * 100); + //console.log('File', file_transfer.filename, 'download', progress, '%'); + this.updateRenderFileTransferBubble(file_transfer, 'Downloaded ' + progress + '%, press to cancel'); + }).done(() => { + console.log('File', file_transfer.filename, 'downloaded'); + delete this.downloadRequests[id]; + RNFS.moveFile(tmp_file_path, file_path).then((success) => { + this.updateRenderFileTransferBubble(file_transfer, 'Download finished'); + this.saveDownloadTask(id, file_transfer.url, file_path); + }) + .catch((err) => { + console.log("Error moving temp file: " + err.message); + console.log("Source: ", tmp_file_path); + console.log("Destination: ", file_path); + file_transfer.local_url = null; + this.fileTransferStateChanged(id, 'failed', file_transfer); + }); + + }).error((error) => { + console.log('File', file_transfer.filename, 'download failed', error); + this.fileTransferStateChanged(id, 'failed', file_transfer); + delete this.downloadRequests[id]; + }); + } + + async decryptFile(file_transfer) { + if (!this.state.keys.private) { + return; + } + + console.log('Decrypt file', file_transfer.local_url); + + let file_path = file_transfer.local_url; + let file_path_decrypted = file_path.slice(0, -4); + + const exists = await RNFS.exists(file_path_decrypted); + + if (exists) { + console.log('File', file_transfer.filename.slice(0, -4), 'is already decrypted'); + } + + this.updateRenderFileTransferBubble(file_transfer, 'Decrypting...'); + + await OpenPGP.decryptFile(file_path, file_path_decrypted, this.state.keys.private, null).then((content) => { + console.log('Decrypted file saved', file_path_decrypted); + file_transfer.local_url = file_path_decrypted; + file_transfer.filename = file_transfer.filename.slice(0, -4); + this.updateFileTransferMessageMetadata(file_transfer, 2); + }).catch((error) => { + console.log('Decrypting file', file_transfer.filename, 'failed:', error.message); + if (file_transfer.filetype.startsWith('text/')) { + RNFS.readFile(file_path, 'utf8').then((content) => { + console.log('Decrypting data', content.substring(0, 80), content.slice(-80)); + OpenPGP.decrypt(content, this.state.keys.private).then((dcontent) => { + console.log('Decrypted', typeof(dcontent), dcontent.length, 'bytes'); + RNFS.writeFile(file_path_decrypted, dcontent, 'utf8').then((success) => { + console.log('Decrypted envelope of', dcontent.length, 'bytes saved to', file_path_decrypted); + file_transfer.local_url = file_path_decrypted; + file_transfer.filename = file_transfer.filename.slice(0, -4); + this.updateFileTransferMessageMetadata(file_transfer, 2); + }).catch((err) => { + console.log('Failed to save decrypted data for', file_transfer.filename, ':', err.message); + this.updateFileTransferMessageMetadata(file_transfer, 3); + }); + }).catch((error) => { + console.log('Decrypting data for', file_transfer.filename, 'error:', error.message); + this.updateFileTransferMessageMetadata(file_transfer, 3); + }); + }).catch((error) => { + this.updateFileTransferMessageMetadata(file_transfer, 3); + console.log('Reading encrypted data for', file_transfer.filename, 'error:', error); + }); + } else { + this.updateFileTransferMessageMetadata(file_transfer, 3); + } + }); + } + async decryptMessage(message, updateContact=false) { // encrypted // 0 not encrypted // null not encrypted // 1 encrypted content // 2 decrypted content // 3 failed to decrypt if (!this.state.keys.private) { return; } let id = message.msg_id; let decryptingMessages = this.state.decryptingMessages; let msg; let pending_messages = []; let idx; await OpenPGP.decrypt(message.content, this.state.keys.private).then((content) => { //console.log('Message', id, message.content_type, 'to', message.to_uri, 'was decrypted'); let messages = this.state.messages; let uri = message.direction === 'incoming' ? message.from_uri : message.to_uri; if (uri in decryptingMessages) { pending_messages = decryptingMessages[uri]; idx = pending_messages.indexOf(id); if (pending_messages.length > 10) { let status = 'Decrypting ' + pending_messages.length + ' messages with'; this._notificationCenter.postSystemNotification(status, {body: uri}); } else if (pending_messages.length === 10) { let status = 'All messages decrypted'; this._notificationCenter.postSystemNotification(status); } if (idx > -1) { pending_messages.splice(idx, 1); decryptingMessages[uri] = pending_messages; this.setState({decryptingMessages: decryptingMessages}); } } if (updateContact) { let myContacts = this.state.myContacts; console.log('Update contact after decryption', uri); if (this.mustPlayIncomingSoundAfterSync) { this.playMessageSound(); this.mustPlayIncomingSoundAfterSync = false; } if (message.timestamp > myContacts[uri].timestamp) { - myContacts[uri].lastMessage = content.substring(0, 100); + myContacts[uri].lastMessage = this.buildLastMessage(message); myContacts[uri].lastMessageId = message.id; myContacts[uri].timestamp = message.timestamp; this.saveSylkContact(uri, myContacts[uri], 'decryptMessage'); this.setState({myContacts: myContacts}); } } if (uri in messages) { let render_messages = messages[uri]; if (message.content_type === 'text/html') { content = utils.html2text(content); } else if (message.content_type === 'text/plain') { content = content; } else if (message.content_type.indexOf('image/') > -1) { - image = `data:${message.content_type};base64,${btoa(content)}` + message.image = `data:${message.content_type};base64,${btoa(content)}` } - msg = this.sql2GiftedChat(message, content); + msg = utils.sql2GiftedChat(message, content); render_messages.push(msg); messages[uri] = render_messages; if (pending_messages.length === 0) { this.confirmRead(uri); this.setState({message: messages}); } } let params = [content, id]; this.ExecuteQuery("update messages set encrypted = 2, content = ? where msg_id = ?", params).then((result) => { //console.log('SQL updated message decrypted', id); }).catch((error) => { console.log('SQL message update error:', error); }); }).catch((error) => { let params = [id]; this.ExecuteQuery("update messages set encrypted = 3 where msg_id = ?", params).then((result) => { - console.log('SQL failed to decrypt message', id, 'rows affected', result.rowsAffected); + console.log('SQL failed to decrypt message', id); }).catch((error) => { console.log('SQL message update error:', error); }); }); } lookupPublicKey(contact) { if (contact.uri.indexOf('@guest') > -1) { return; } if (contact.uri.indexOf('anonymous') > -1) { return; } if (!contact.publicKey && !contact.conference && this.state.connection) { this.state.connection.lookupPublicKey(contact.uri); } } async contactsCount() { let query = "SELECT count(*) as rows FROM contacts where account = ?"; let rows; let total; await this.ExecuteQuery(query, [this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'total contacts'); }).catch((error) => { console.log('SQL error:', error); }); } - async getMessages(uri, pinned=false) { + async getMessages(uri, filter={pinned: false, category: null}) { + + console.log('Get messages', filter); + + let pinned=filter && 'pinned' in filter ? filter['pinned'] : false; + let category=filter && 'category' in filter ? filter['category'] : null; + + // retrieve message from SQL database 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; + let localpath; + let filteredMessageIds = []; if (!uri) { query = "SELECT count(*) as rows FROM messages where (from_uri = ? and direction = 'outgoing') or (to_uri = ? and direction = 'incoming')"; await this.ExecuteQuery(query, [this.state.accountId, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; console.log(total, 'total messages'); }).catch((error) => { console.log('SQL error:', error); }); return; } orig_uri = uri; if (Object.keys(myContacts).indexOf(uri) === -1) { this.setState({messages: {}}); return; } if (utils.isPhoneNumber(uri) && uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } else { this.resetUnreadCount(orig_uri); this.lookupPublicKey(myContacts[orig_uri]); } //console.log('Get messages with', uri, 'with zoom factor', this.state.messageZoomFactor); let limit = this.state.messageLimit * this.state.messageZoomFactor; query = "SELECT count(*) as rows FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?))"; if (pinned) { query = query + ' and pinned = 1'; } + if (category && category !== 'text') { + query = query + " and metadata != ''"; + } + await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId]).then((results) => { rows = results.rows; total = rows.item(0).rows; - //console.log('Got', total, 'messages with', uri, 'from database'); + //console.log('Got', total, 'messages with', uri, 'from database', ); }).catch((error) => { console.log('SQL error:', error); }); if (uri in myContacts) { myContacts[uri].totalMessages = total; } query = "SELECT * FROM messages where ((from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?)) "; if (pinned) { query = query + ' and pinned = 1'; } + if (category && category !== 'text') { + query = query + " and metadata != ''"; + } + query = query + ' order by unix_timestamp desc limit ?, ?'; await this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId, this.state.messageStart, limit]).then((results) => { //console.log('SQL get messages, rows =', results.rows.length); let rows = results.rows; messages[orig_uri] = []; let content; let ts; let last_message; let last_message_id; let last_direction; let messages_to_decrypt = []; let decryptingMessages = {}; let msg; let enc; + let file_path; + let file_transfer; let last_content = null; for (let i = 0; i < rows.length; i++) { var item = rows.item(i); if (false) { //console.log('Remove broken message', item); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } content = item.content; - if (!content && !item.image) { + if (!content) { content = 'Empty message...'; } last_direction = item.direction; let timestamp; last_message = null; last_message_id = null; let unix_timestamp; if (item.unix_timestamp === 0) { timestamp = JSON.parse(item.timestamp, _parseSQLDate); unix_timestamp = Math.floor(timestamp / 1000); item.unix_timestamp = unix_timestamp; this.ExecuteQuery('update messages set unix_timestamp = ? where msg_id = ?', [unix_timestamp, item.msg_id]); } else { timestamp = new Date(item.unix_timestamp * 1000); } + const is_encrypted = content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && content.indexOf('-----END PGP MESSAGE-----') > -1; if (is_encrypted) { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; if (item.encrypted === null) { item.encrypted = 1; } /* encrypted: 1 = unencrypted 2 = decrypted 3 = failed to decrypt message */ enc = parseInt(item.encrypted); if (enc && enc !== 3 ) { if (uri in decryptingMessages) { } else { decryptingMessages[orig_uri] = []; } decryptingMessages[orig_uri].push(item.msg_id); messages_to_decrypt.push(item); } } else { if (item.content_type === 'text/html') { content = utils.html2text(content); } else if (item.content_type === 'text/plain') { content = content; + } else if (item.content_type === 'application/sylk-file-transfer') { + content = content; } else if (item.content_type.indexOf('image/') > -1) { - image = `data:${item.content_type};base64,${btoa(content)}` + item.image = `data:${item.content_type};base64,${btoa(content)}` } else if (item.content_type === 'application/sylk-contact-update') { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; console.log('Remove update contact message', item.id); this.ExecuteQuery('delete from messages where msg_id = ?', [item.msg_id]); continue; + } else if (item.content_type === 'text/pgp-public-key-imported') { + continue; } else { console.log('Unknown message', item.msg_id, 'type', item.content_type); myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; + this.deleteMessage(item.msg_id, item.to_uri); continue; } - //console.log(item); + last_content = content; - if (last_content === content) { + msg = utils.sql2GiftedChat(item, content, filter); + if (!msg) { myContacts[orig_uri].totalMessages = myContacts[orig_uri].totalMessages - 1; continue; } - last_content = content; - - msg = this.sql2GiftedChat(item, content); messages[orig_uri].push(msg); + if (pinned || category) { + filteredMessageIds.push(msg._id); + } + + if (msg.metadata && msg.metadata.filename) { + this.checkFileTransfer(msg.metadata); + } } } + this.setState({filteredMessageIds: filteredMessageIds}); //console.log('Got', messages[orig_uri].length, 'out of', total, 'messages for', uri); last_messages = messages[orig_uri]; last_messages.reverse(); if (last_messages.length > 0) { last_messages.forEach((last_item) => { - if (!last_item.image && !last_item.system && last_item.text) { - last_message = last_item.text.substring(0, 100); - last_message_id = last_item.id; - } else { - return; - } + last_message = this.buildLastMessage(last_item); + last_message_id = last_item.id; + return; }); } if (orig_uri in myContacts) { if (last_message && last_message != myContacts[orig_uri].lastMessage) { myContacts[orig_uri].lastMessage = last_message; myContacts[orig_uri].lastMessageId = last_message_id; this.saveSylkContact(uri, myContacts[orig_uri], 'getMessages'); this.setState({myContacts: myContacts}); } } this.setState({messages: messages, decryptingMessages: decryptingMessages}); let i = 1; messages_to_decrypt.forEach((item) => { var updateContact = messages_to_decrypt.length === i; //console.log('To decrypt', messages_to_decrypt.length, 'updateContact =', updateContact); this.decryptMessage(item, updateContact); i = i + 1; }); }).catch((error) => { - console.log('SQL error:', error); + console.log('getMessages SQL error:', error); }); + } async deleteSsiCredential(contact) { this.setState({ selectedContact: null, targetUri: '' }); if (this.ssiAgent) { await this.ssiAgent.credentials.deleteById(contact.uri); utils.timestampedLog('Deleted SSI credential', contact.uri); const credentials = await this.ssiAgent.credentials.getAll(); this.setState({ssiCredentials: credentials}); } } async deleteSsiConnection(contact) { this.setState({ selectedContact: null, targetUri: '' }); if (this.ssiAgent) { await this.ssiAgent.connections.deleteById(contact.uri); utils.timestampedLog('Deleted SSI connection', contact.uri); const connections = await this.ssiAgent.connections.getAll(); this.setState({ssiConnections: connections}); } } - async deleteMessages(uri, local=true) { + async deleteMessages(uri, remote=false) { console.log('Delete messages for', uri); - let myContacts = this.state.myContacts; + + if (this.state.filteredMessageIds.length > 0) { + this.state.filteredMessageIds.forEach((id) => { + this.deleteMessage(id, uri, remote); + }); + return; + } + let messages = this.state.messages; + let myContacts = this.state.myContacts; + let query; let params; let orig_uri = uri; if (uri) { if (uri.indexOf('@') === -1 && utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } else { - if (local) { + if (remote) { + console.log('Delete messages remote party', uri); this.addJournal(orig_uri, 'removeConversation'); } } } if (uri) { let dir = RNFS.DocumentDirectoryPath + '/conference/' + uri + '/files'; RNFS.unlink(dir).then((success) => { console.log('Removed folder', dir); }).catch((err) => { //console.log('Error deleting folder', dir, err.message); }); query = "DELETE FROM messages where ((from_uri = ? and to_uri = ? and direction = 'incoming') or (from_uri = ? and to_uri = ? and direction = 'outgoing'))"; params = [uri, this.state.accountId, this.state.accountId, uri]; } else { console.log('--- Wiping device --- '); let dir = RNFS.DocumentDirectoryPath + '/conference/'; RNFS.unlink(dir).then((success) => { console.log('Removed folder', dir); }).catch((err) => { //console.log('Error deleting folder', dir, err.message); }); query = "DELETE FROM messages where (account = ? and to_uri = ? and direction = 'incoming') or (account = ? and from_uri = ? and direction = 'outgoing')"; params = [this.state.accountId, this.state.accountId, this.state.accountId, this.state.accountId]; this.setState({messages: {}}); this.saveLastSyncId(null); } await this.ExecuteQuery(query, params).then((result) => { if (result.rowsAffected) { - console.log('SQL deleted', result.rowsAffected, 'messages'); + console.log('deleteMessages SQL deleted', result.rowsAffected, 'messages'); if (uri) { this._notificationCenter.postSystemNotification(result.rowsAffected + ' messages removed'); } } if (!uri) { this.deleteAllContacts(this.state.accountId); } else { if (result.rowsAffected === 0) { - this.removeContact(orig_uri); + //this.removeContact(orig_uri); } else { if (orig_uri in messages) { delete messages[orig_uri]; this.setState({messages: messages}); } if (orig_uri in myContacts) { myContacts[orig_uri].totalMessages = 0; myContacts[orig_uri].lastMessage = null; myContacts[orig_uri].lastMessageId = null; this.setState({myContacts: myContacts}); } } } }).catch((error) => { - console.log('SQL query:', query); - console.log('SQL error:', error); + console.log('deleteMessages SQL error:', error); }); } async deleteAllContacts(account) { let query = 'delete from contacts where account = ?'; this.setState({myContacts: {}}); await this.ExecuteQuery(query, [account]).then((result) => { if (result.rowsAffected) { console.log('SQL deleted', result.rowsAffected, 'contacts'); } this.deleteKeys(account); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } async deleteKeys(account) { let query = 'delete from keys where account = ?'; this.setState({keys: null}); await this.ExecuteQuery(query, [account]).then((result) => { if (result.rowsAffected) { console.log('SQL deleted', result.rowsAffected, 'keys'); } setTimeout(() => { this.logout(); }, 1000); }).catch((error) => { console.log('SQL query:', query); console.log('SQL error:', error); }); } playMessageSound(direction='incoming') { let must_play_sound = true; if (direction === 'incoming') { if (this.incoming_sound_ts) { let diff = (Date.now() - this.incoming_sound_ts)/ 1000; if (diff < 5) { must_play_sound = false; } } } else { if (this.outgoing_sound_ts) { let diff = (Date.now() - this.outgoing_sound_ts)/ 1000; if (diff < 5) { must_play_sound = false; } } } if (!must_play_sound) { console.log('Play incoming sound skipped'); } if (Platform.OS === 'android' && this.state.appState === 'foreground') { // } try { if (Platform.OS === 'ios') { SoundPlayer.setSpeaker(true); } //SoundPlayer.playSoundFile('message_received', 'wav'); if (direction === 'incoming') { this.incoming_sound_ts = Date.now(); SoundPlayer.playSoundFile('beluga_in', 'wav'); } else { this.outgoing_sound_ts = Date.now(); SoundPlayer.playSoundFile('beluga_out', 'wav'); } } catch (e) { console.log('Error playing', direction,' sound:', e); } } async removeMessage(message, uri=null) { if (uri === null) { uri = message.sender.uri; } await this.deleteMessage(message.id, uri, false).then((result) => { console.log('Message', message.id, 'to', uri, 'is removed'); }).catch((error) => { //console.log('Failed to remove message', message.id, 'to', uri); return; }); let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) === -1) { return; } let existingMessages = renderMessages[uri]; let newMessages = []; existingMessages.forEach((msg) => { - if (msg.id === message.id) { + 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); + console.log('Sync remove conversation with', uri, 'before', obj.timestamp); let query; let unix_timestamp = Math.floor(obj.timestamp / 1000); query = "DELETE FROM messages where (from_uri = ? and to_uri = ?) or (from_uri = ? and to_uri = ?) and (unix_timestamp < ? or unix_timestamp = 0)"; this.ExecuteQuery(query, [this.state.accountId, uri, uri, this.state.accountId, unix_timestamp]).then((result) => { if (result.rowsAffected > 0) { console.log('SQL deleted', result.rowsAffected, 'messages with', uri, 'before', obj.timestamp); } }).catch((error) => { console.log('SQL delete conversation sync error:', error); }); let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].timestamp < obj.timestamp) { this.deleteSylkContact(uri); } } async readConversation(obj) { let uri = obj; this.resetUnreadCount(uri) } removeContact(uri) { + console.log('removeContact', uri); let myContacts = this.state.myContacts; this.deleteSylkContact(uri); if (this.state.selectedContact && this.state.selectedContact.uri === uri) { this.setState({selectedContact: null}); } let renderMessages = this.state.messages; if (uri in renderMessages) { delete renderMessages[uri]; this.setState({messages: renderMessages}); } } add_sync_pending_item(item) { if (this.sync_pending_items.indexOf(item) > -1) { return; } this.sync_pending_items.push(item); if (this.sync_pending_items.length == 1) { //console.log('Sync started ---'); this.setState({syncConversations: true}); if (this.syncTimer === null) { this.syncTimer = setTimeout(() => { this.resetSyncTimer(); }, 1000 * 60); } } } resetSyncTimer() { if (this.sync_pending_items.length > 0) { this.sync_pending_items = []; console.log('Sync ended by timer ---'); //console.log('Pending tasks:', this.sync_pending_items); this.afterSyncTasks(); } } remove_sync_pending_item(item) { //console.log('remove_sync_pending_item', this.sync_pending_items.length); let idx = this.sync_pending_items.indexOf(item); if (idx > -1) { this.sync_pending_items.splice(idx, 1); } if (this.sync_pending_items.length == 0 && this.state.syncConversations) { if (this.syncTimer !== null) { clearTimeout(this.syncTimer); this.syncTimer = null; } this.afterSyncTasks(); } else { if (this.sync_pending_items.length > 10 && this.sync_pending_items.length % 10 == 0) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } else if (this.sync_pending_items.length > 0 && this.sync_pending_items.length < 10) { //console.log(this.sync_pending_items.length, 'sync items remaining'); } } } - async insertPendingMessages() { - let query = "INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, pending, sent, received) VALUES " - - if (this.pendingNewSQLMessages.length > 0) { - //console.log('Inserting', this.pendingNewSQLMessages.length, 'new messages'); - } - - let pendingNewSQLMessages = this.pendingNewSQLMessages; - this.pendingNewSQLMessages = []; - - let all_values = []; - let n = 0; - let i = 1; - - if (pendingNewSQLMessages.length > 0) { - pendingNewSQLMessages.forEach((values) => { - Array.prototype.push.apply(all_values, values); - query = query + "("; - n = 0; - while (n < values.length ) { - query = query + "?" - if (n < values.length - 1) { - query = query + ","; - } - n = n + 1; - } - query = query + ")"; - if (pendingNewSQLMessages.length > i) { - query = query + ", "; - } - i = i + 1; - }); - - this.ExecuteQuery(query, all_values).then((result) => { - //console.log('SQL inserted', pendingNewSQLMessages.length, 'messages'); - this.newSyncMessagesCount = this.newSyncMessagesCount + pendingNewSQLMessages.length; - }).catch((error) => { - console.log('SQL error inserting bulk messages:', error.message); - console.log('query:', query); - pendingNewSQLMessages.forEach((values) => { - this.ExecuteQuery(query, values).then((result) => { - this.newSyncMessagesCount = this.newSyncMessagesCount + 1; - }).catch((error) => { - if (error.message.indexOf('SQLITE_CONSTRAINT_PRIMARYKEY') > -1) { - console.log('Duplicate message id', values[2]); - } else { - console.log('SQL error inserting message', values[2], error.message); - } - }); - }); - }); - } - } - async afterSyncTasks() { this.insertPendingMessages(); if (this.newSyncMessagesCount) { console.log('Synced', this.newSyncMessagesCount, 'messages from server'); this.newSyncMessagesCount = 0; } this.setState({syncConversations: false}); this.sync_pending_items = []; let myContacts = this.state.myContacts; let updateContactUris = this.state.updateContactUris; let replicateContacts = this.state.replicateContacts; let deletedContacts = this.state.deletedContacts; //console.log('updateContactUris:', Object.keys(updateContactUris).toString()); //console.log('replicateContacts:', Object.keys(replicateContacts).toString()); //console.log('deletedContacts:', Object.keys(deletedContacts).toString()); let uris = Object.keys(replicateContacts).concat(Object.keys(updateContactUris)); uris = [... new Set(uris)]; //console.log('Update contacts:', uris.toString()); // sync changed myContacts with SQL database let created; let old_tags; uris.forEach((uri) => { if (uri in myContacts) { created = false; } else { if (uri in deletedContacts) { return } myContacts[uri] = this.newContact(uri); created = true; } if (uri in replicateContacts) { myContacts[uri].name = replicateContacts[uri].name; myContacts[uri].email = replicateContacts[uri].email; myContacts[uri].organization = replicateContacts[uri].organization; old_tags = myContacts[uri].tags; myContacts[uri].tags = replicateContacts[uri].tags; myContacts[uri].participants = replicateContacts[uri].participants; if (myContacts[uri].timestamp > replicateContacts[uri].timestamp) { if (old_tags.indexOf('missed') > -1 && replicateContacts[uri].tags.indexOf('missed') === -1) { myContacts[uri].tags.push('missed'); } } if (old_tags.indexOf('chat') > -1 && replicateContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (old_tags.indexOf('history') > -1 && replicateContacts[uri].tags.indexOf('history') === -1) { myContacts[uri].tags.push('history'); } if (replicateContacts[uri].timestamp > myContacts[uri].timestamp || created) { myContacts[uri].timestamp = replicateContacts[uri].timestamp; if (uri === this.state.accountId) { let name = replicateContacts[uri].name || ''; let organization = replicateContacts[uri].organization || ''; this.setState({displayName: name, organization: organization, email: myContacts[uri].email}); } } } if (uri in updateContactUris && updateContactUris[uri] > myContacts[uri].timestamp) { myContacts[uri].timestamp = updateContactUris[uri]; } this.saveSylkContact(uri, myContacts[uri], 'syncEnd'); }); let purgeMessages = this.state.purgeMessages; purgeMessages.forEach((id) => { this.deleteMessage(id, this.state.accountId); }); Object.keys(deletedContacts).forEach((uri) => { this.removeConversationSync(deletedContacts[uri]) }); this.setState({purgeMessages:[], syncConversations: false, firstSyncDone: true, updateContactUris: {}, replicateContacts: {}, deletedContacts: {}}); if (this.syncStartTimestamp) { let diff = (Date.now() - this.syncStartTimestamp)/ 1000; this.syncStartTimestamp = null; //console.log('Sync ended after', diff, 'seconds'); } setTimeout(() => { this.addTestContacts(); this.refreshNavigationItems(); this.updateServerHistory('syncConversations') if (this.state.selectedContact) { this.getMessages(this.state.selectedContact.uri); } }, 1000); } async syncConversations(messages) { if (this.sync_pending_items.length > 0) { console.log('Sync already in progress'); return; } if (this.mustLogout || this.currentRoute === '/logout') { return; } if (this.currentRoute === '/login') { return; } this.syncStartTimestamp = new Date(); let myContacts = this.state.myContacts; let renderMessages = this.state.messages; if (messages.length > 0) { utils.timestampedLog('Sync', messages.length, 'message events from server'); //this._notificationCenter.postSystemNotification('Syncing messages with the server'); this.add_sync_pending_item('sync_in_progress'); } else { this.setState({firstSyncDone: true}); utils.timestampedLog('Sync messages ended'); setTimeout(() => { this.addTestContacts(); this.refreshNavigationItems(); this.updateServerHistory('syncConversations') }, 500); } let i = 0; let idx; let uri; let last_id; let content; let contact; let existingMessages; let formatted_date; let newMessages = []; let lastMessages = {}; let updateContactUris = {}; let deletedContacts = {}; let last_timestamp; let stats = {state: 0, remove: 0, incoming: 0, outgoing: 0, delete: 0, read: 0} + let gMsg; let purgeMessages = this.state.purgeMessages; messages.forEach((message) => { if (this.mustLogout) { return; } last_timestamp = message.timestamp; i = i + 1; uri = null; if (message.contentType === 'application/sylk-message-remove') { uri = message.content.contact; } else if (message.contentType === 'application/sylk-conversation-remove') { uri = message.content; } else if (message.contentType === 'application/sylk-conversation-read' ) { uri = message.content; } else if (message.contentType === 'message/imdn') { } else { if (message.sender.uri === this.state.account.id) { uri = message.receiver; } else { uri = message.sender.uri; } } - if (uri) { - //console.log('Process journal', i, 'of', messages.length, message.contentType, uri, message.timestamp); - } + //console.log('Process journal', i, 'of', messages.length, message.contentType, uri, message.timestamp); let d = new Date(2019); if (message.timestamp < d) { console.log('Skip broken journal with broken date', message.id); purgeMessages.push(message.id); return; } if (!message.content) { console.log('Skip broken journal with empty body', message.id); purgeMessages.push(message.id); return; } if (message.contentType !== 'application/sylk-conversation-remove' && message.contentType !== 'application/sylk-message-remove' && uri && Object.keys(myContacts).indexOf(uri) === -1) { if (uri.indexOf('@') > -1 && !utils.isEmailAddress(uri)) { + console.log('Skip bad uri', uri); return; } console.log('Will add a new contact', uri); myContacts[uri] = this.newContact(uri); myContacts[uri].timestamp = message.timestamp; //this.setState({myContacts: myContacts}); } //console.log('Sync', message.timestamp, message.contentType, uri); if (message.contentType === 'application/sylk-message-remove') { idx = 'remove' + message.id; this.add_sync_pending_item(idx); this.deleteMessageSync(message.id, uri); if (uri in renderMessages) { existingMessages = renderMessages[uri]; newMessages = []; existingMessages.forEach((msg) => { - if (msg.id === message.id) { + 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 (message.contentType === 'application/sylk-file-transfer') { + gMsg = utils.sylk2GiftedChat(message, '', 'incoming'); + myContacts[uri].lastMessage = this.buildLastMessage(gMsg); + myContacts[uri].lastMessageId = message.id; + myContacts[uri].lastCallDuration = null; + myContacts[uri].direction = 'incoming'; + } + if (this.state.selectedContact && this.state.selectedContact.uri === uri) { this.mustPlayIncomingSoundAfterSync = true; } if (myContacts[uri].tags.indexOf('chat') === -1 && (message.contentType === 'text/plain' || message.contentType === 'text/html')) { myContacts[uri].tags.push('chat'); } lastMessages[uri] = message.id; if (message.dispositionNotification.indexOf('display') > -1) { myContacts[uri].unread.push(message.id); } stats.incoming = stats.incoming + 1; this.incomingMessageSync(message); } } last_id = message.id; }); this.updateTotalUread(myContacts); /* if (messages.length > 0) { Object.keys(stats).forEach((key) => { console.log('Sync', stats[key], key); }); } */ this.setState({messages: renderMessages, updateContactUris: updateContactUris, deletedContacts: deletedContacts, purgeMessages: purgeMessages }); this.remove_sync_pending_item('sync_in_progress'); Object.keys(lastMessages).forEach((uri) => { + //console.log('Last messages update:' , lastMessages); //console.log('Update last message for', uri); // TODO update lastMessage content for each contact }); if (last_id) { this.saveLastSyncId(last_id, true); } } async publicKeyReceived(message) { if (message.publicKey) { this.savePublicKey(message.uri, message.publicKey.trim()); } else { console.log('No public key available on server for', message.uri); if (message.uri === this.state.accountId) { var uri = uuid.v4() + '@' + this.state.defaultDomain; //console.log('Send 1st public to', uri); this.sendPublicKey(uri); } } } async incomingMessage(message) { console.log('Message', message.id, message.contentType, 'was received'); // Handle incoming messages this.saveLastSyncId(message.id); if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'application/sylk-contact-update') { return; } if (message.contentType === 'text/pgp-public-key') { this.savePublicKey(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-private-key' && message.sender.uri === this.state.account.id) { console.log('Received PGP private key from another device'); this.processRemotePrivateKey(message.content); return; } // This URLs are used to request SSI credentials if (message.content.startsWith('https://didcomm.issuer.bloqzone.com?c_i=')) { this.handleSSIEnrolment(message.content); this.saveSystemMessage(message.sender.uri, 'SSI enrolment proposal received', 'incoming'); //return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; if (is_encrypted) { if (!this.state.keys || !this.state.keys.private) { console.log('Missing private key, cannot decrypt message'); this.sendDispositionNotification(message, 'error'); this.saveSystemMessage(message.sender.uri, 'Cannot decrypt message, no private key', 'incoming'); } else { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Incoming message', message.id, 'decrypted'); this.handleIncomingMessage(message, decryptedBody); }).catch((error) => { console.log('Failed to decrypt message', message.id, error); this.sendPublicKey(message.sender.uri); this.sendDispositionNotification(message, 'error'); this.saveSystemMessage(message.sender.uri, 'Cannot decrypt last message, wrong key', 'incoming'); }); } } else { //console.log('Incoming message is not encrypted'); this.handleIncomingMessage(message); } } handleIncomingMessage(message, decryptedBody=null) { + console.log('handleIncomingMessage') let content = decryptedBody || message.content; - if (!this.state.selectedContact || this.state.selectedContact.uri != message.sender.uri) { this.postAndroidMessageNotification(message.sender.uri, content); } this.saveIncomingMessage(message, decryptedBody); let renderMessages = this.state.messages; + let gMsg = utils.sylk2GiftedChat(message, decryptedBody, 'incoming'); + if (this.state.selectedContact) { if (message.sender.uri === this.state.selectedContact.uri) { if (message.sender.uri in renderMessages) { - if (renderMessages[message.sender.uri].some((obj) => obj.id === message.id)) { + if (renderMessages[message.sender.uri].some((obj) => obj._id === message.id)) { return; } } else { renderMessages[message.sender.uri] = []; } - } - renderMessages[message.sender.uri].push(utils.sylkToRenderMessage(message, decryptedBody, 'incoming')); + renderMessages[message.sender.uri].push(gMsg); + let selectedContact = this.state.selectedContact; + selectedContact.lastMessage = this.buildLastMessage(gMsg); + selectedContact.timestamp = message.timestamp; + selectedContact.direction = 'incoming'; + selectedContact.lastCallDuration = null; - 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}); + } - this.setState({selectedContact: selectedContact, messages: renderMessages}); } else { this.setState({messages: renderMessages}); } if (this.state.selectedContact || this.currentRoute === '/ready') { this.playMessageSound(); } this.notifyIncomingMessageWhileInACall(message.sender.uri); } + buildLastMessage(message, content=null) { + let new_content = ''; + let filename = 'File'; + //console.log('buildLastMessage'); + + if (message.contentType === 'application/sylk-file-transfer') { + new_content = utils.beautyFileNameForBubble(message.metadata, true); + } else { + new_content = content || message.content || message.text; + } + + let c = new_content.substring(0, 100); + return c; + } + async incomingMessageSync(message) { //console.log('Sync incoming message', message); // Handle incoming messages if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); this.savePublicKeySync(message.sender.uri, message.content); return; } if (message.contentType === 'text/pgp-public-key-imported') { return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { this.saveIncomingMessageSync(message, null, true); } else { //console.log('Incoming message', message.id, 'not encrypted from', message.sender.uri); this.saveIncomingMessageSync(message); } this.remove_sync_pending_item(message.id); } async outgoingMessage(message) { - console.log('Outgoing message', message.contentType, message.id, 'to', message.receiver); + console.log('--- Outgoing message', message.contentType, message.id, 'to', message.receiver); this.saveLastSyncId(message.id); + let gMsg; if (message.content.indexOf('?OTRv3') > -1) { return; } if (message.contentType === 'text/pgp-public-key') { return; } if (message.sender.uri.indexOf('@conference') > -1) { return; } if (message.sender.uri.indexOf('@videoconference') > -1) { return; } if (message.contentType === 'text/pgp-public-key-imported') { this.hideExportPrivateKeyModal(); this.hideImportPrivateKeyModal(); return; } if (message.contentType === 'message/imdn') { return; } if (message.contentType === 'text/pgp-private-key' && message.sender.uri === this.state.account.id) { console.log('Received my own PGP private key'); this.processRemotePrivateKey(message.content); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Outgoing message', message.id, 'decrypted to', message.receiver, message.contentType); content = decryptedBody; if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContact(content); } else { + this.saveOutgoingMessageSql(message, content, 1); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } - if (message.contentType === 'text/html') { - content = utils.html2text(content); - } else if (message.contentType.indexOf('image/') > -1) { - content = 'Image'; - } + let gMsg = utils.sylk2GiftedChat(message, content, 'outgoing'); if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { - myContacts[uri].lastMessage = content.substring(0, 100); + myContacts[uri].lastMessage = this.buildLastMessage(gMsg); myContacts[uri].lastMessageId = message.id; if (this.state.selectedContact) { let selectedContact = this.state.selectedContact; selectedContact.lastMessage = myContacts[uri].lastMessage; selectedContact.timestamp = message.timestamp; selectedContact.direction = 'outgoing'; selectedContact.lastCallDuration = null; this.setState({selectedContact: selectedContact}); } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > -1) { - if (!renderMessages[uri].some((obj) => obj.id === message.id)) { - renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); + if (!renderMessages[uri].some((obj) => obj._id === message.id)) { + renderMessages[uri].push(gMsg); + //console.log('Added render message', message.id, message.contentType); this.setState({renderMessages: renderMessages}); } else { return; } } } this.setState({myContacts: myContacts}); this.saveSylkContact(uri, myContacts[uri], 'outgoingMessage'); } }).catch((error) => { console.log('Failed to decrypt my own message in outgoingMessage:', error); return; }); } else { if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContact(content); } else { + this.saveOutgoingMessageSql(message); let myContacts = this.state.myContacts; let uri = message.receiver; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri); } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { - content = 'Image'; + content = 'Photo'; } + gMsg = utils.sylk2GiftedChat(message, content, 'outgoing') + if (content && content.indexOf('-----BEGIN PGP MESSAGE-----') === -1) { - myContacts[uri].lastMessage = content.substring(0, 100); + myContacts[uri].lastMessage = this.buildLastMessage(gMsg); + myContacts[uri].lastMessageId = message.id; } let renderMessages = this.state.messages; + //console.log(renderMessages); if (Object.keys(renderMessages).indexOf(uri) > -1) { - if (!renderMessages[uri].some((obj) => obj.id === message.id)) { - renderMessages[uri].push(utils.sylkToRenderMessage(message, content, 'outgoing')); + if (!renderMessages[uri].some((obj) => obj._id === message.id)) { + renderMessages[uri].push(gMsg); + //console.log('Added render message', message.id, message.contentType); this.setState({renderMessages: renderMessages}); } else { return; } } this.saveSylkContact(uri, myContacts[uri], 'outgoingMessage'); } } } async outgoingMessageSync(message) { //console.log('Sync outgoing message', message.id, 'to', message.receiver); if (message.content.indexOf('?OTRv3') > -1) { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-public-key') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'message/imdn') { this.remove_sync_pending_item(message.id); return; } if (message.contentType === 'text/pgp-private-key') { this.remove_sync_pending_item(message.id); return; } const is_encrypted = message.content.indexOf('-----BEGIN PGP MESSAGE-----') > -1 && message.content.indexOf('-----END PGP MESSAGE-----') > -1; let content = message.content; if (is_encrypted) { if (message.contentType === 'application/sylk-contact-update') { await OpenPGP.decrypt(message.content, this.state.keys.private).then((decryptedBody) => { //console.log('Sync outgoing message', message.id, message.contentType, 'decrypted to', message.receiver); this.handleReplicateContactSync(decryptedBody, message.id, message.timestamp); this.remove_sync_pending_item(message.id); }).catch((error) => { console.log('Failed to decrypt my own message in sync:', error); this.remove_sync_pending_item(message.id); return; }); } else { this.saveOutgoingMessageSqlBatch(message, null, true); this.remove_sync_pending_item(message.id); } } else { if (message.contentType === 'application/sylk-contact-update') { this.handleReplicateContactSync(content, message.id, message.timestamp); this.remove_sync_pending_item(message.id); } else { this.saveOutgoingMessageSqlBatch(message); } } } saveOutgoingMessageSql(message, decryptedBody=null, is_encrypted=false) { + console.log('saveOutgoingMessageSql'); + let pending = 0; let sent = null; let received = null; let encrypted = 0; let content = decryptedBody || message.content; + let metadata; + + if (message.contentType === 'application/sylk-file-transfer') { + message.metadata = content; + try { + metadata = JSON.parse(message.metadata); + } catch (e) { + console.log('saveOutgoingMessageSql error parsing json', message.metadata); + } + + } else { + message.metadata = ''; + } if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } const failed_states = ['failed', 'error', 'forbidden']; if (message.state == 'pending') { pending = 1; } else if (message.state == 'accepted') { pending = 0; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (failed_states.indexOf(message.state) > -1) { sent = 1; received = 0; } else { console.log('Invalid state for message', message.id, message.state); return; } let ts = message.timestamp; let unix_timestamp = Math.floor(ts / 1000); - let params = [this.state.accountId, encrypted, message.id, JSON.stringify(ts), unix_timestamp, content, message.contentType, message.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); + let params = [this.state.accountId, encrypted, message.id, JSON.stringify(ts), unix_timestamp, content, message.contentType, message.metadata, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; + this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, pending, sent, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { + console.log('SQL inserted outgoing', message.contentType, 'message to', message.receiver, 'encrypted =', encrypted); this.remove_sync_pending_item(message.id); + + if (message.contentType === 'application/sylk-file-transfer') { + if (metadata) { + this.updateRenderFileTransferBubble(metadata); + this.checkFileTransfer(metadata); + } + } + }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { - console.log('SQL error:', error); + console.log('saveOutgoingMessageSql SQL error:', error); + } else { + if (message.contentType === 'application/sylk-file-transfer') { + this.updateSqlFileTransferMessage(message.id, content, pending, sent, received, message.state); + } } this.remove_sync_pending_item(message.id); }); } + async updateFileTransferMessageMetadata(metadata, encryption=0) { + let query = "SELECT * from messages where msg_id = ?"; + await this.ExecuteQuery(query, [metadata.transfer_id]).then((results) => { + let rows = results.rows; + if (rows.length === 1) { + if (encryption === 3) { + metadata.decryption_failed = true; + } else { + metadata.decryption_failed = false; + } + var item = rows.item(0); + let params = [JSON.stringify(metadata), encryption, metadata.transfer_id] + query = "update messages set metadata = ?, encrypted = ? where msg_id = ?" + this.ExecuteQuery(query, params).then((results) => { + //console.log('SQL updated file transfer metadata', metadata); + this.updateRenderFileTransferBubble(metadata); + }).catch((error) => { + console.log('updateFileTransferMessageMetadata SQL error:', error); + }); + } + + }).catch((error) => { + console.log('updateFileTransferMessageMetadata SQL error:', error); + }); + } + + async updateSqlFileTransferMessage(id, content, pending, sent, received, state) { + let query = "SELECT * from messages where msg_id = ?"; + await this.ExecuteQuery(query, [id]).then((results) => { + let rows = results.rows; + if (rows.length === 1) { + var item = rows.item(0); + var new_metadata = JSON.parse(content); + let old_metadata = JSON.parse(item.metadata); + new_metadata.local_url = old_metadata.local_url; + console.log('File transfer', new_metadata.transfer_id, 'available at', new_metadata.url); + let params = [content, JSON.stringify(new_metadata), pending, sent, received, id] + query = "update messages set content = ?, metadata = ?, pending = ?, sent = ?, received = ? where msg_id = ?" + this.ExecuteQuery(query, params).then((results) => { + //console.log('SQL updated file transfer', id); + this.checkFileTransfer(new_metadata); + // to do, skip query done below + this.updateRenderMessageState(id, state); + }).catch((error) => { + console.log('updateFileTransferMessage SQL error:', error); + }); + } + + }).catch((error) => { + console.log('updateFileTransferMessage SQL error:', error); + }); + } + async saveOutgoingMessageSqlBatch(message, decryptedBody=null, is_encrypted=false) { let pending = 0; let sent = 0; let received = null; let failed = 0; let encrypted = 0; let content = decryptedBody || message.content; + if (message.contentType === 'application/sylk-file-transfer') { + message.metadata = content; + } else { + message.metadata = ''; + } + if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } if (message.state == 'pending') { pending = 1; } else if (message.state == 'delivered') { sent = 1; } else if (message.state == 'displayed') { received = 1; sent = 1; } else if (message.state == 'failed') { sent = 1; received = 0; failed = 1; } else if (message.state == 'error') { sent = 1; received = 0; failed = 1; } else if (message.state == 'forbidden') { sent = 1; received = 0; } let unix_timestamp = Math.floor(message.timestamp / 1000); - let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, message.receiver, "outgoing", pending, sent, received]; + let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.metadata, message.sender.uri, message.receiver, "outgoing", pending, sent, received, message.state]; this.pendingNewSQLMessages.push(params); - if (this.pendingNewSQLMessages.length > 34) { + if (this.pendingNewSQLMessages.length > 24) { this.insertPendingMessages(); } this.remove_sync_pending_item(message.id); } + async insertPendingMessages() { + let query = "INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, pending, sent, received, state) VALUES " + + //if (this.pendingNewSQLMessages.length > 0) { + console.log('Inserting', this.pendingNewSQLMessages.length, 'new messages'); + //} + + let pendingNewSQLMessages = this.pendingNewSQLMessages; + this.pendingNewSQLMessages = []; + + let all_values = []; + let n = 0; + let i = 1; + + let pending = 0; + let sent = null; + let received = null; + let state = null; + let content = null; + let metadata = null; + let id = null; + let account = null; + const failed_states = ['failed', 'error', 'forbidden']; + + if (pendingNewSQLMessages.length > 0) { + pendingNewSQLMessages.forEach((values) => { + Array.prototype.push.apply(all_values, values); + query = query + "("; + n = 0; + while (n < values.length ) { + query = query + "?" + if (n < values.length - 1) { + query = query + ","; + } + n = n + 1; + } + query = query + ")"; + if (pendingNewSQLMessages.length > i) { + query = query + ", "; + } + i = i + 1; + }); + + this.ExecuteQuery(query, all_values).then((result) => { + console.log('SQL inserted', pendingNewSQLMessages.length, 'messages'); + this.newSyncMessagesCount = this.newSyncMessagesCount + pendingNewSQLMessages.length; + }).catch((error) => { + console.log('SQL error inserting bulk messages:', error.message); + pendingNewSQLMessages.forEach((values) => { + this.ExecuteQuery(query, values).then((result) => { + this.newSyncMessagesCount = this.newSyncMessagesCount + 1; + }).catch((error) => { + id = values[2]; + if (error.message.indexOf('SQLITE_CONSTRAINT_PRIMARYKEY') > -1) { + // todo update file transfer status + if (values[6] === 'application/sylk-file-transfer') { + content = values[5]; + state = values[14]; + if (state == 'pending') { + pending = 1; + } else if (state == 'accepted') { + pending = 0; + } else if (state == 'delivered') { + sent = 1; + } else if (state == 'received') { + received = 1; + } else if (state == 'displayed') { + received = 1; + sent = 1; + } else if (failed_states.indexOf(state) > -1) { + sent = 1; + received = 0; + } + this.updateSqlFileTransferMessage(id, content, pending, sent, received, state); + } + + } else { + console.log('SQL error inserting message', id, error.message); + } + }); + }); + }); + } + } + async saveSystemMessage(uri, content, direction, missed=false) { let timestamp = new Date(); let unix_timestamp = Math.floor(timestamp / 1000); let id = uuid.v4(); let params = [this.state.accountId, id, JSON.stringify(timestamp), unix_timestamp, content, 'text/plain', direction === 'incoming' ? uri : this.state.account.id, direction === 'outgoing' ? uri : this.state.account.id, 0, 1, direction]; await this.ExecuteQuery("INSERT INTO messages (account, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, pending, system, direction) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { this.renderSystemMessage(uri, content, direction, timestamp); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { console.log('SQL error:', error); } }); } async renderPurchasePSTNCredit(uri) { let url = 'https://mdns.sipthor.net/sip_settings.phtml?account='+ this.state.accountId + '&tab=credit'; let myContacts = this.state.myContacts; if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { uri = uri.split('@')[0]; } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > - 1) { let msg; msg = { _id: uuid.v4(), text: 'To call phone numbers, you must purchase credit at ' + url, createdAt: new Date(), direction: 'incoming', sent: true, pending: false, failed: false, user: {_id: uri, name: uri} } renderMessages[uri].push(msg); this.setState({renderMessages: renderMessages}); } } + updateRenderFileTransferBubble(metadata, text=null) { + if (!this.state.selectedContact) { + return; + } + + let id = metadata.transfer_id; + + let renderMessages = this.state.messages; + let existingMessages = renderMessages[this.state.selectedContact.uri]; + let newMessages = []; + + if (!existingMessages) { + return; + } + + existingMessages.forEach((msg) => { + if (msg._id === id) { + msg.text = text || utils.beautyFileNameForBubble(metadata); + if (metadata.decryption_failed) { + msg.text = msg.text + ' (decryption failed)'; + msg.failed = true; + } + msg.metadata = metadata; + if (!metadata.local_url || metadata.decryption_failed || metadata.local_url.endsWith('.asc')) { + msg.image = null; + msg.video = null; + msg.audio = null; + } else { + if (utils.isImage(metadata.filename)) { + msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isAudio(metadata.filename)) { + msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isVideo(metadata.filename, metadata)) { + msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } + } + //console.log('updateRenderFileTransferBubble', msg.text); + } + newMessages.push(msg); + }); + + renderMessages[this.state.selectedContact.uri] = newMessages; + this.setState({messages: renderMessages}); + } + async renderSystemMessage(uri, content, direction, timestamp) { let myContacts = this.state.myContacts; if (Object.keys(myContacts).indexOf(uri) === -1 && utils.isPhoneNumber(uri) && uri.indexOf('@') > -1) { uri = uri.split('@')[0]; } let renderMessages = this.state.messages; if (Object.keys(renderMessages).indexOf(uri) > - 1) { let msg; msg = { _id: uuid.v4(), text: content, createdAt: timestamp || new Date(), direction: direction, sent: true, pending: false, system: true, failed: false, user: direction == 'incoming' ? {_id: uri, name: uri} : {} } renderMessages[uri].push(msg); this.setState({renderMessages: renderMessages}); } } async saveIncomingMessage(message, decryptedBody=null) { let myContacts = this.state.myContacts; let uri = message.sender.uri; if (uri in myContacts) { // } else { myContacts[uri] = this.newContact(uri, message.ssiName); if (message.ssiName) { myContacts[uri].tags.push('ssi'); } } if (myContacts[uri].tags.indexOf('blocked') > -1) { return; } var content = decryptedBody || message.content; let received = 1; let unix_timestamp = Math.floor(message.timestamp / 1000); let encrypted = decryptedBody === null ? 0 : 2; - let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, message.sender.uri, this.state.account.id, "incoming", received]; - await this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, from_uri, to_uri, direction, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { + let metadata = message.contentType === 'application/sylk-file-transfer' ? message.content : ''; + + let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, metadata, message.sender.uri, this.state.account.id, "incoming", received]; + await this.ExecuteQuery("INSERT INTO messages (account, encrypted, msg_id, timestamp, unix_timestamp, content, content_type, metadata, from_uri, to_uri, direction, received) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params).then((result) => { if (myContacts[uri].name === null || myContacts[uri].name === '' && message.sender.displayName) { myContacts[uri].name = message.sender.displayName; } if (message.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = message.timestamp; } myContacts[uri].unread.push(message.id); myContacts[uri].direction = 'incoming'; myContacts[uri].lastCallDuration = null; if (myContacts[uri].tags.indexOf('chat') === -1) { myContacts[uri].tags.push('chat'); } if (myContacts[uri].totalMessages) { myContacts[uri].totalMessages = myContacts[uri].totalMessages + 1; } if (message.contentType === 'text/html') { content = utils.html2text(content); } else if (message.contentType.indexOf('image/') > -1) { - content = '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}); + content = 'Photo'; + } else if (message.contentType === 'application/sylk-file-transfer') { + try { + this.checkFileTransfer(JSON.parse(metadata)); + } catch (e) { + console.log("Error decoding incoming file transfer json sql: ", e); + } } this.updateTotalUread(myContacts); this.saveSylkContact(uri, myContacts[uri], 'saveIncomingMessage'); }).catch((error) => { if (error.message.indexOf('UNIQUE constraint failed') === -1) { - console.log('SQL error:', error); + console.log('saveIncomingMessage SQL error:', error); } }); } saveIncomingMessageSync(message, decryptedBody=null, is_encrypted=false) { var content = decryptedBody || message.content; let encrypted = 0; if (decryptedBody !== null) { encrypted = 2; } else if (is_encrypted) { encrypted = 1; } let received = 0; let imdn_msg; //console.log('saveIncomingMessageSync', message); if (message.dispositionNotification.indexOf('display') === -1) { //console.log('Incoming message', message.id, 'was already read'); received = 2; } else { if (message.dispositionNotification.indexOf('positive-delivery') > -1) { imdn_msg = {id: message.id, timestamp: message.timestamp, from_uri: message.sender.uri} if (this.sendDispositionNotification(imdn_msg, 'delivered')) { received = 1; } } else { received = 1; } } let pending let sent; let unix_timestamp = Math.floor(message.timestamp / 1000); - - let 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]; + let metadata = message.contentType === 'application/sylk-file-transfer' ? message.content : ''; + //console.log('Sync metadata', message.id, message.contentType, metadata, typeof(message.content)); + let params = [this.state.accountId, encrypted, message.id, JSON.stringify(message.timestamp), unix_timestamp, content, message.contentType, metadata, message.sender.uri, this.state.account.id, "incoming", pending, sent, received, message.state]; this.pendingNewSQLMessages.push(params); this.remove_sync_pending_item(message.id); - if (this.pendingNewSQLMessages.length > 34) { + if (this.pendingNewSQLMessages.length > 24) { this.insertPendingMessages() } } saveParticipant(callUUID, room, uri) { if (this._historyConferenceParticipants.has(callUUID)) { let old_participants = this._historyConferenceParticipants.get(callUUID); if (old_participants.indexOf(uri) === -1) { old_participants.push(uri); } } else { let new_participants = [uri]; this._historyConferenceParticipants.set(callUUID, new_participants); } if (!this.myParticipants) { this.myParticipants = new Object(); } if (this.myParticipants.hasOwnProperty(room)) { let old_uris = this.myParticipants[room]; if (old_uris.indexOf(uri) === -1 && uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { this.myParticipants[room].push(uri); } } else { let new_uris = []; if (uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { new_uris.push(uri); } if (new_uris) { this.myParticipants[room] = new_uris; } } storage.set('myParticipants', this.myParticipants); } deleteContact(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { this.deleteMessages(uri); } } deletePublicKey(uri) { uri = uri.trim().toLowerCase(); if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { myContacts[uri].publicKey = null; console.log('Public key of', uri, 'deleted'); this.saveSylkContact(uri, myContacts[uri], 'deletePublicKey'); } } newContact(uri, name=null, data={}) { //console.log('Create new contact', uri, data); let current_datetime = new Date(); if (data.src !== 'init') { uri = uri.trim().toLowerCase(); } let contact = { id: uuid.v4(), uri: uri, name: name || data.name || '', organization: data.organization || '', unread: [], tags: [], lastCallMedia: [], participants: [], timestamp: current_datetime } contact = this.sanitizeContact(uri, contact, data); return contact; } newSyntheticContact(uri, name=null, data={}) { //console.log('Create new contact', uri, data); let current_datetime = new Date(); uri = uri.trim().toLowerCase(); let contact = { id: uuid.v4(), uri: uri, name: name || data.name || '', organization: data.organization || '', unread: [], tags: ['synthetic'], lastCallMedia: [], participants: [], timestamp: current_datetime } return contact; } updateTotalUread(myContacts=null) { let total_unread = 0; myContacts = myContacts || this.state.myContacts; Object.keys(myContacts).forEach((uri) => { total_unread = total_unread + myContacts[uri].unread.length; }); //console.log('Total unread messages', total_unread) if (Platform.OS === 'ios') { PushNotification.setApplicationIconBadgeNumber(total_unread); } else { ShortcutBadge.setCount(total_unread); } } saveContact(uri, displayName='', organization='', email='') { displayName = displayName.trim(); uri = uri.trim().toLowerCase(); let contact; if (uri.indexOf('@') === -1 && !utils.isPhoneNumber(uri)) { uri = uri + '@' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts) { contact = myContacts[uri]; } else { contact = this.newContact(uri); if (!contact) { return; } } contact.organization = organization; contact.name = displayName; contact.uri = uri; contact.email = email; contact.timestamp = new Date(); contact = this.sanitizeContact(uri, contact); if (!contact) { this._notificationCenter.postSystemNotification('Invalid contact ' + uri); return; } if (!contact.photo) { var name_idx = contact.name.trim().toLowerCase(); if (name_idx in this.state.avatarPhotos) { contact.photo = this.state.avatarPhotos[name_idx]; } } this.replicateContact(contact); this.saveSylkContact(uri, contact, 'saveContact'); let selectedContact = this.state.selectedContact; if (selectedContact && selectedContact.uri === uri) { selectedContact.displayName = displayName; selectedContact.organization = organization; this.setState({selectedContact: selectedContact}); } if (uri === this.state.accountId) { this.setState({displayName: displayName, email: email}); this.signup[this.state.accountId] = email; storage.set('signup', this.signup); if (this.state.account && displayName !== this.state.account.displayName) { this.processRegistration(this.state.accountId, this.state.password, displayName); } } } async replicateContact(contact) { //console.log('Replicate contact', contact); if (!this.state.keys) { console.log('Cannot replicate contact without a private key'); return; } let id = uuid.v4(); let content; let contentType = 'application/sylk-contact-update'; let new_contact = {} new_contact.uri = contact.uri; new_contact.name = contact.name; new_contact.email = contact.email; new_contact.organization = contact.organization; new_contact.timestamp = Math.floor(contact.timestamp / 1000); new_contact.tags = contact.tags; new_contact.participants = contact.participants; content = JSON.stringify(new_contact); //this.saveOutgoingRawMessage(id, this.state.accountId, this.state.accountId, content, contentType); await OpenPGP.encrypt(content, this.state.keys.public).then((encryptedMessage) => { this._sendMessage(this.state.accountId, encryptedMessage, id, contentType, contact.timestamp); }).catch((error) => { console.log('Failed to encrypt contact:', error); }); } handleReplicateContact(json_contact) { let contact; let new_contact; - contact = JSON.parse(json_contact); + + try { + contact = JSON.parse(json_contact); + } catch (e) { + console.log("Failed to parse contact json: ", e); + return; + } if (contact.uri === null) { return; } if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } let uri = contact.uri; let myContacts = this.state.myContacts; if (uri in myContacts) { new_contact = myContacts[uri]; // } else { new_contact = this.newContact(uri, contact.name); if (!new_contact) { return; } } new_contact.uri = uri; new_contact.name = contact.name; new_contact.email = contact.email; new_contact.organization = contact.organization; new_contact.timestamp = new Date(contact.timestamp * 1000); new_contact.tags = contact.tags; new_contact.participants = contact.participants; this.saveSylkContact(uri, new_contact, 'handleReplicateContact'); } async handleReplicateContactSync(json_contact, id, msg_timestamp) { let purgeMessages = this.state.purgeMessages; let contact; - contact = JSON.parse(json_contact); + try { + contact = JSON.parse(json_contact); + } catch (e) { + console.log("Failed to parse contact json: ", e); + return; + } + let timestamp = msg_timestamp; let uri = contact.uri; if (contact.uri === this.state.accountId) { this.setState({displayName: contact.name, organization: contact.organization, email: contact.email}); this.signup[this.state.accountId] = contact.email; storage.set('signup', this.signup); } if (contact.timestamp) { timestamp = new Date(contact.timestamp * 1000); } let replicateContacts = this.state.replicateContacts; if (uri in replicateContacts) { if (timestamp < replicateContacts[uri].timestamp) { purgeMessages.push(id); this.setState({purgeMessages: purgeMessages}); //console.log('Sync replicate contact skipped because is too old', timestamp, uri); return; } else { purgeMessages.push(replicateContacts[uri].msg_id); this.setState({purgeMessages: purgeMessages}); //console.log('Sync replicate contact is newer', timestamp, 'than', replicateContacts[uri].timestamp, 'remove previous one', replicateContacts[uri].msg_id); } // } else { let new_contact = this.newContact(uri, contact.name); if (!new_contact) { this.remove_sync_pending_item(id); purgeMessages.push(id); this.setState({purgeMessages: purgeMessages}); return; } replicateContacts[uri] = new_contact; } console.log('Sync replicate contact', uri); replicateContacts[uri].uri = uri; replicateContacts[uri].msg_id = id; replicateContacts[uri].name = contact.name; replicateContacts[uri].email = contact.email; replicateContacts[uri].timestamp = timestamp; replicateContacts[uri].organization = contact.organization; replicateContacts[uri].tags = contact.tags; replicateContacts[uri].participants = contact.participants; //console.log('Adding replicated contact', replicateContacts[uri]); this.setState({replicateContacts: replicateContacts}); this.remove_sync_pending_item(id); } sanitizeContact(uri, contact, data={}) { //console.log('sanitizeContact', uri, contact); let idx; if (!uri || uri === '') { return null; } if (data.src !== 'init') { uri = uri.trim().toLowerCase(); } let domain; let els = uri.split('@'); let username = els[0]; let isNumber = utils.isPhoneNumber(username); let uuidPattern = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; let isUUID = uri.match(uuidPattern); if (!isUUID && !isNumber && !utils.isEmailAddress(uri) && username !== '*') { console.log('Sanitize check failed for uri:', uri); return null; } contact.uri = uri; if (!contact.conference) { contact.conference = false; } if (!contact.tags) { contact.tags = []; } contact.tags = [... new Set(contact.tags)]; if (contact.direction === 'received'){ contact.direction = 'incoming'; } else if (contact.direction === 'placed') { contact.direction = 'outgoing'; } if (xtype(contact.timestamp) !== 'date') { contact.timestamp = new Date(); } if (!contact.participants) { contact.participants = []; } contact.participants = [... new Set(contact.participants)]; if (!contact.unread) { contact.unread = []; } contact.unread = [... new Set(contact.unread)]; if (!contact.lastCallMedia) { contact.lastCallMedia = []; } contact.lastCallMedia = [... new Set(contact.lastCallMedia)]; return contact; } updateFavorite(uri, favorite) { if (favorite === null) { return; } let favoriteUris = this.state.favoriteUris; let idx; idx = favoriteUris.indexOf(uri); if (favorite && idx === -1) { favoriteUris.push(uri); this.setState({favoriteUris: favoriteUris, refreshFavorites: !this.state.refreshFavorites}); } else if (!favorite && idx > -1) { favoriteUris.splice(idx, 1); this.setState({favoriteUris: favoriteUris, refreshFavorites: !this.state.refreshFavorites}); } else { return; } } toggleFavorite(uri) { //console.log('toggleFavorite', uri); let favoriteUris = this.state.favoriteUris; let myContacts = this.state.myContacts; let selectedContact; let favorite; if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } idx = myContacts[uri].tags.indexOf('favorite'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); favorite = false; } else { myContacts[uri].tags.push('favorite'); favorite = true; } myContacts[uri].timestamp = new Date(); this.saveSylkContact(uri, myContacts[uri], 'toggleFavorite'); let idx = favoriteUris.indexOf(uri); if (idx === -1 && favorite) { favoriteUris.push(uri); console.log(uri, 'is favorite'); } else if (idx > -1 && !favorite) { favoriteUris.splice(idx, 1); console.log(uri, 'is not favorite'); } this.replicateContact(myContacts[uri]); this.setState({favoriteUris: favoriteUris}); } toggleBlocked(uri) { let blockedUris = this.state.blockedUris; let myContacts = this.state.myContacts; if (uri.indexOf('@guest.') > -1) { uri = 'anonymous@anonymous.invalid'; } if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } let blocked; idx = myContacts[uri].tags.indexOf('blocked'); if (idx > -1) { myContacts[uri].tags.splice(idx, 1); blocked = false; } else { myContacts[uri].tags.push('blocked'); blocked = true; } myContacts[uri].timestamp = new Date(); this.saveSylkContact(uri, myContacts[uri], 'toggleBlocked'); let idx = blockedUris.indexOf(uri); if (idx === -1 && blocked) { blockedUris.push(uri); } else if (idx > -1 && !blocked) { blockedUris.splice(idx, 1); } this.replicateContact(myContacts[uri]); this.setState({blockedUris: blockedUris, selectedContact: null}); } updateBlocked(uri, blocked) { if (blocked === null) { return; } let blockedUris = this.state.blockedUris; let idx; idx = blockedUris.indexOf(uri); if (blocked && idx === -1) { blockedUris.push(uri); this.setState({blockedUris: blockedUris}); } else if (!blocked && idx > -1) { blockedUris.splice(idx, 1); this.setState({blockedUris: blockedUris}); } else { return; } } appendInvitedParties(room, uris) { //console.log('Save invited parties', uris, 'for room', room); let myInvitedParties = this.state.myInvitedParties; let current_uris = myInvitedParties.hasOwnProperty(room) ? myInvitedParties[room] : []; uris.forEach((uri) => { let idx = current_uris.indexOf(uri); if (idx === -1) { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } if (uri !== this.state.account.id) { current_uris.push(uri); //console.log('Added', uri, 'to room', room); } } }); this.saveConference(room, uris); } - shareContent() { + async shareContent() { + console.log('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; } + var id = uuid.v4(); + + let msg = { + _id: id, + key: id, + text: content, + createdAt: new Date(), + direction: 'outgoing', + user: {} + } + content = content.trim(); + let contentType = 'text/plain'; + + if (item.filePath) { + contentType = 'application/sylk-file-transfer'; + const { size } = await RNFetchBlob.fs.stat(item.filePath); + + let file_transfer = { 'path': item.filePath, + 'filename': item.fileName, + 'filetype' : item.mimeType, + 'filesize': size, + 'sender': {'uri': this.state.accountId}, + 'receiver': {'uri': null}, + 'transfer_id': id, + 'direction': 'outgoing' + }; + + msg.metadata = file_transfer; + + if (utils.isImage(item.fileName)) { + msg.image = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; + } else if (utils.isAudio(item.fileName)) { + msg.audio = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; + } else if (utils.isVideo(item.fileName)) { + msg.video = Platform.OS === "android" ? 'file://'+ item.filePath : item.filePath; + } + + if (content.length > 0) { + content = content + ' + ' + utils.beautyFileNameForBubble(file_transfer); + } else { + content = utils.beautyFileNameForBubble(file_transfer); + } + } + + ReceiveSharingIntent.clearReceivedFiles(); this.state.selectedContacts.forEach((uri) => { - let msg = this.textToGiftedMessage(content); - console.log('Share external item with', uri); - this.sendMessage(uri, msg); + if (msg.metadata) { + msg.metadata.receiver.uri = uri; + } + console.log('Out msg', msg); + this.sendMessage(uri, msg, contentType); }); - ReceiveSharingIntent.clearReceivedFiles(); this.setState({shareContent: [], selectedContacts: [], shareToContacts: false}); } filterHistory(filter) { //console.log('Filter history', filter); this.setState({historyFilter: filter}); } saveConference(room, participants, displayName=null) { let uri = room; console.log('Save conference', room, 'with display name', displayName, 'and participants', participants); let myContacts = this.state.myContacts; if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } myContacts[uri].timestamp = new Date(); myContacts[uri].name = displayName; let new_participants = []; participants.forEach((uri) => { if (uri.indexOf('@') === -1) { uri = uri + '@' + this.state.defaultDomain; } if (uri !== this.state.account.id) { new_participants.push(uri); console.log('Added', uri, 'to room', room); } }); myContacts[uri].participants = new_participants; this.replicateContact(myContacts[uri]); this.saveSylkContact(uri, myContacts[uri], 'saveConference'); } addHistoryEntry(uri, callUUID, direction='outgoing', participants=[]) { let myContacts = this.state.myContacts; //console.log('addHistoryEntry', uri); if (uri.indexOf('@') === -1) { uri = uri + '@videoconference.' + this.state.defaultDomain; } if (uri in myContacts) { } else { myContacts[uri] = this.newContact(uri); } myContacts[uri].conference = true; myContacts[uri].timestamp = new Date(); myContacts[uri].lastCallId = callUUID; myContacts[uri].direction = direction; this.saveSylkContact(uri, myContacts[uri], 'addHistoryEntry'); } updateHistoryEntry(uri, callUUID, duration) { if (uri.indexOf('@') === -1) { uri = uri + '@videoconference.' + this.state.defaultDomain; } let myContacts = this.state.myContacts; if (uri in myContacts && myContacts[uri].lastCallId === callUUID) { console.log('updateHistoryEntry', uri, callUUID, duration); myContacts[uri].timestamp = new Date(); myContacts[uri].lastCallDuration = duration; myContacts[uri].lastCallId = callUUID; this.replicateContact(myContacts[uri]) this.saveSylkContact(uri, myContacts[uri], 'updateHistoryEntry'); } } render() { let footerBox = ; let extraStyles = {}; if (this.state.localMedia || this.state.registrationState === 'registered') { footerBox = null; } let loadingLabel = this.state.loading; if (this.state.syncConversations) { - loadingLabel = 'Sync conversations'; + //loadingLabel = 'Sync conversations'; + } else if (this.state.reconnectingCall) { loadingLabel = 'Reconnecting call...'; } else if (this.mustLogout) { loadingLabel = 'Logging out...'; } return ( this.setState({ Width_Layout : event.nativeEvent.layout.width, Height_Layout : event.nativeEvent.layout.height }, ()=> this._detectOrientation())}> { Platform.OS === 'android' ? : null} ); } notFound(match) { const status = { title : '404', message : 'Oops, the page your looking for can\'t found', level : 'danger', width : 'large' } return ( ); } saveHistory(history) { let myContacts = this.state.myContacts; let missedCalls = this.state.missedCalls; let localTime; let tags = []; let uri; let i = 0; let idx; let contact; let must_save = false; history.forEach((item) => { uri = item.uri; must_save = false; if (this.state.blockedUris.indexOf(uri) > -1) { return; } if (uri in myContacts) { } else { contact = this.newContact(uri); if (!contact) { console.log('No valid contact for', uri); return; } myContacts[uri] = contact; let contacts = this.lookupContacts(uri) if (contacts.length > 0) { myContacts[uri].name = contacts[0].name; myContacts[uri].tags = contacts[0].tags; myContacts[uri].photo = contacts[0].photo; myContacts[uri].label = contacts[0].label; } myContacts[uri].timestamp = item.timestamp; must_save = true; } if (item.timestamp > myContacts[uri].timestamp) { myContacts[uri].timestamp = item.timestamp; must_save = true; } else { if (myContacts[uri].lastCallId === item.sessionId) { return; } else { must_save = true; } } tags = myContacts[uri].tags; if (item.tags.indexOf('missed') > - 1) { tags.push('missed'); myContacts[uri].unread.push(item.sessionId); if (missedCalls.indexOf(item.sessionId) === -1) { missedCalls.push(item.sessionId); must_save = true; } } else { idx = tags.indexOf('missed'); if (idx > -1) { tags.splice(idx, 1); must_save = true; } } tags.push('history'); if (item.displayName && !myContacts[uri].name) { myContacts[uri].name = item.displayName; must_save = true; } myContacts[uri].direction = item.direction; myContacts[uri].lastCallId = item.sessionId; myContacts[uri].lastCallDuration = item.duration; myContacts[uri].lastCallMedia = item.media; myContacts[uri].conference = item.conference; if (tags !== myContacts[uri].tags) { must_save = true; } myContacts[uri].tags = tags; i = i + 1; this.updateTotalUread(myContacts); if (must_save) { this.saveSylkContact(uri, this.state.myContacts[uri], 'saveHistory'); } }); this.setState({missedCalls: missedCalls}); } hideLogsModal() { this.setState({showLogsModal: false}); } purgeLogs() { RNFS.unlink(logfile) .then(() => { utils.timestampedLog('Log file initialized'); this.showLogs(); }) // `unlink` will throw an error, if the item to unlink does not exist .catch((err) => { console.log(err.message); }); } showLogs() { this.setState({showLogsModal: true}); RNFS.readFile(logfile, 'utf8').then((content) => { console.log('Read', content.length, 'bytes from', logfile); const lastlines = content.split('\n').slice(-MAX_LOG_LINES).join('\n'); this.setState({logs: lastlines}); }); } trimLogs() { RNFS.readFile(logfile, 'utf8').then((content) => { const lines = content.split('\n'); //console.log('Read', lines.length, 'lines and', content.length, 'bytes from', logfile); if (lines.length > (MAX_LOG_LINES + 50) || content.length > 100000) { const text = lines.slice(-MAX_LOG_LINES).join('\n'); RNFS.writeFile(logfile, text + '\r\n', 'utf8') .then((success) => { //console.log('Trimmed logs to', MAX_LOG_LINES, 'lines and', text.length, 'bytes'); }) .catch((err) => { console.log(err.message); }); } }); } ready() { let publicKey; let call = this.state.currentCall || this.state.incomingCall; if (this.state.selectedContact) { const uri = this.state.selectedContact.uri; if (uri in this.state.myContacts && this.state.myContacts[uri].publicKey) { publicKey = this.state.myContacts[uri].publicKey; } } else { publicKey = this.state.keys ? this.state.keys.public: null; } return ( ); } preview() { return ( ); } saveSlider(position) { this.setState({conferenceSliderPosition: position}); } call() { let call = this.state.currentCall || this.state.incomingCall; let callState; if (call && call.id in this.state.callsState) { callState = this.state.callsState[call.id]; } if (this.state.targetUri in this.state.myContacts && !this.state.callContact) { let callContact = this.state.myContacts[this.state.targetUri]; this.setState({callContact: callContact}); } return ( ) } postSystemNotification(msg) { if (!this._notificationCenter) { return; } this._notificationCenter.postSystemNotification(msg); } conference() { let _previousParticipants = new Set(); let call = this.state.currentCall || this.state.incomingCall; let callState; if (call && call.id in this.state.callsState) { callState = this.state.callsState[call.id]; } /* if (this.myParticipants) { let room = this.state.targetUri.split('@')[0]; if (this.myParticipants.hasOwnProperty(room)) { let uris = this.myParticipants[room]; if (uris) { uris.forEach((uri) => { if (uri.search(this.state.defaultDomain) > -1) { let user = uri.split('@')[0]; _previousParticipants.add(user); } else { _previousParticipants.add(uri); } }); } } } */ if (this.state.myInvitedParties) { if (this.state.myInvitedParties.hasOwnProperty(this.state.targetUri)) { let uris = this.state.myInvitedParties[this.state.targetUri]; if (uris) { uris.forEach((uri) => { _previousParticipants.add(uri); }); } } } let previousParticipants = Array.from(_previousParticipants); return ( ) } matchContact(contact, filter='', matchDisplayName=true) { if (contact.uri.toLowerCase().startsWith(filter.toLowerCase())) { return true; } if (matchDisplayName && contact.name && contact.name.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return true; } return false; } lookupContacts(text) { let contacts = []; const addressbook_contacts = this.state.contacts.filter(contact => this.matchContact(contact, text)); addressbook_contacts.forEach((c) => { const existing_contacts = contacts.filter(contact => this.matchContact(contact, c.uri.toLowerCase(), false)); if (existing_contacts.length === 0) { contacts.push(c); } }); return contacts; } updateLoading(state, by='') { if (by === 'incoming_call_timeout') { console.log('Incoming call timeout'); } else if (this.state.loading === incomingCallLabel && by !== 'incoming_call' && this.state.incomingCallUUID) { console.log('Skip updateLoading because we wait for a call', this.state.loading); return; } else if (by === 'incoming_call' && this.state.loading && this.state.loading !== incomingCallLabel) { console.log('Skip updateLoading by incoming_call', this.state.loading); return; } //console.log('updateLoading', this.state.loading, '->', state, 'by', by); this.setState({loading: state}); } conferenceByUri(urlParameters) { const targetUri = utils.normalizeUri(urlParameters.targetUri, config.defaultConferenceDomain); const idx = targetUri.indexOf('@'); const uri = {}; const pattern = /^[A-Za-z0-9\-\_]+$/g; uri.user = targetUri.substring(0, idx); // check if the uri.user is valid if (!pattern.test(uri.user)) { const status = { title : 'Invalid conference', message : `Oops, the conference ID is invalid: ${targetUri}`, level : 'danger', width : 'large' } return ( ); } return ( ); } login() { let registerBox; let statusBox; this.mustLogout = false; if (this.state.status !== null) { statusBox = ( ); } if (this.state.registrationState !== 'registered') { registerBox = ( ); } return ( {registerBox} {statusBox} ); } logout() { this.syncRequested = false; this.callKeeper.setAvailable(false); this.sql_contacts_keys = []; // SSI wallet - cleanup if (!this.mustLogout && this.state.registrationState !== null && this.state.connection && this.state.connection.state === 'ready') { // remove token from server this.mustLogout = true; //console.log('Remove push token'); this.state.account.setDeviceToken('None', Platform.OS, deviceId, true, bundleId); //console.log('Unregister'); this.state.account.register(); return; } else if (this.mustLogout && this.state.connection && this.state.account) { //console.log('Unregister'); this.state.account.unregister(); } this.tokenSent = false; if (this.state.connection && this.state.account) { //console.log('Remove account'); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { logger.debug(error); } }); } storage.set('account', {accountId: this.state.accountId, password: this.state.password, verified: false }); this.setState({account: null, displayName: '', ssiAgent: null, email: '', loading: null, keyStatus: {}, contactsLoaded: false, registrationState: null, registrationKeepalive: false, keyExistsOnServer: false, keyDifferentOnServer: false, status: null, keys: null, lastSyncId: null, accountVerified: false, autoLogin: false, myContacts: {}, defaultDomain: config.defaultDomain, purgeMessages: [], updateContactUris: {}, replicateContacts: {}, deletedContacts: {} }); this.mustLogout = false; this.ssiAgent = null; this.changeRoute('/login', 'user logout'); return null; } main() { return null; } } export default Sylk; diff --git a/app/assets/styles/blink/_ConferenceBox.scss b/app/assets/styles/blink/_ConferenceBox.scss index 155c5e5..2711e3f 100644 --- a/app/assets/styles/blink/_ConferenceBox.scss +++ b/app/assets/styles/blink/_ConferenceBox.scss @@ -1,200 +1,230 @@ .container { flex: 1; } .roundshape { height: 48; //any of height width: 48; //any of width justifyContent: center; borderRadius: 24px; } .whiteButton { background-color: white; } .disabledWhiteButton { background-color: rgba(#395936, .9); } .greenButton { background-color: rgba(#6DAA63, .9); } .disabledGreenButton { background-color: rgba(#395936, .9); } .hangupButton { background-color: rgba(#a94442, .8); } .buttonsContainer { flex-direction: row; justify-content: center; height: 55; border: 0px; } .hangupButtonAudioContainer { flex-direction: row; justify-content: center; margin-right: 15px; height: 55; border: 0px; } .hangupButtonVideoContainer { margin-top: 15px; } .hangupButtonVideoContainerLandscape { margin-right: 15px; } .conferenceContainerLandscape { flex-direction: row; align-content: flex-end; position: absolute; bottom: 0; left: 0; right: 0; top: 0; } .audioContainerLandscape { align-content: flex-start; margin-top: 55px; margin-left: 0px; margin-right: 5px; width: 50%; border: 0px; } .chatContainerLandscapeAudio { margin-top: 60px; margin-right: 0px; margin-left: 5px; border-color: grey; border-radius: 5px; width: 49%; border: 0px; } .chatContainerLandscape { margin-top: 65px; width: 400px; } .slider { height: 30px; justify-content: center; } .dotsContainer { height: 8px; flex-direction: row; justify-content: space-between; } .dots { margin-top: -19px; margin-right: 0px; } .dotsiOS { margin-top: -17px; margin-right: 0px; } .audioContainerPortrait { border: 0.5px; } .chatContainerPortraitAudio { align-content: flex-end; } .chatContainerPortrait { flex: 1; border: 0px; border-radius: 5px; } .conferenceContainer { flex-direction: column; align-content: flex-start; position: absolute; bottom: 0; left: 0; right: 0; top: 0; } .videosContainer { flex: 1; flex-wrap: wrap; border-radius: 5px; } .carouselContainer { position: absolute; bottom: 10; left: 10; right: 10; } .landscapeVideosContainer { flex-direction: row; } .downloadContainer { flex-direction: row; justify-content: flex-end; align-items: center; } .switch { padding: 10px; } .uploadProgress { font-size: 14px; color: orange; } .button { background-color: white; margin: 8px; } .iosButton { background-color: white; margin: 8px; padding-top: 4px; } .androidButton { background-color: white; margin: 8px; } .hangupButton { background-color: rgba(#a94442, .8); } .wholePageVideo { width: 100%; height: 100%; } .landscapeDrawer { align-items: flex-end; justify-content: space-between; flex-direction: row; } .portraitDrawer { width: 300px; } + +.chatSendContainer { + flex-direction: row; +} + +.chatPapperclip { + marginBottom: 15; + marginRight: 30; + transform: rotateY(180deg); + border: 0px; +} + +.chatSendArrow { + marginBottom: 15; + marginRight: 10; +} + +.videoContainer { + width: 100%; +} + +.audioContainer { + width: 100%; +} + +.videoPlayer { +} + +.audioPlayer { +} diff --git a/app/assets/styles/blink/_ContactsListBox.scss b/app/assets/styles/blink/_ContactsListBox.scss index ed73c59..933df5f 100644 --- a/app/assets/styles/blink/_ContactsListBox.scss +++ b/app/assets/styles/blink/_ContactsListBox.scss @@ -1,48 +1,123 @@ .portraitContainer { flex: 1; flex-direction: column; } .lock { margin-left: 3px; margin-top: 2px; } .contactsPortraitContainer { flex: 1; flex-direction: column; } .chatPortraitContainer { flex: 6; margin-top: 15px; } .landscapeContainer { flex: 1; flex-direction: row; } .contactsLandscapeContainer { flex: 1; flex-direction: row; } .chatLandscapeContainer { flex: 1; } .chatBorder { border-width: 0px; margin: 1px; border-radius: 5px; border-color: gray; } .backgroundVideo { position: absolute; top: 0; left: 0; bottom: 0; right: 0; } + +.chatSendContainer { + flex-direction: row; +} + +.chatRightActionsContainer { + marginBottom: 15; + marginRight: 30; + transform: rotateY(180deg); + border: 0px; +} + +.chatInsideRightActionsContainer { + marginBottom: 0; + border: 0px; +} + +.chatAudioIcon { + marginBottom: 10; +} + +.chatLeftActionsContainer { + marginBottom: 5; + width: 40; + marginLeft: 10; + border: 0px; +} + + +.chatSendArrow { + marginBottom: 10; + marginRight: 10; + border: 0px; +} + + +.chatIconText { + color: '#b2b2b2'; + weight: 'bold'; + fontSize: 15; + backgroundColor: 'transparent'; + textAlign: 'center'; +} + +.bubbleContainer { +} + +.videoContainer { + width: 100%; +} + +.audioContainer { + margin-top: 10; + width: 100%; +} + +.videoPlayer { +} + +.audioPlayer { +} + +.roundshape { + height: 48; //any of height + width: 48; //any of width + justifyContent: center; + borderRadius: 24px; +} + +.playAudioButton { + background-color: rgba(#4572a6, 1); +} + +.chatImage { +} diff --git a/app/assets/styles/blink/_ReadyBox.scss b/app/assets/styles/blink/_ReadyBox.scss index 476c453..f4b830e 100644 --- a/app/assets/styles/blink/_ReadyBox.scss +++ b/app/assets/styles/blink/_ReadyBox.scss @@ -1,213 +1,214 @@ @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; margin-left: 20%; margin-right: 20%; margin-top: 10px; margin-bottom: 10px; 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: 0px; margin-top: 3px; margin-right: 5px; } .landscapeTabletUriInputBox { align: left; padding-top: 10px; padding-bottom: 10px; width: 66%; margin-left: 1px; } .landscapeButtonGroup { flex: 1; flex-direction: row; justify-content: center; padding: 15px; } .portraitTabletUriInputBox { align: left; padding-top: 10px; width: 100%; } .uriInputBox { align: left; padding-top: 10px; width: 100%; } .buttonGroup { flex-direction: row; justify-content: center; padding-left: 20px; padding-right: 20px; padding-top: 10px; padding-bottom: 10px; } .buttonContainer { justifyContent: center; alignItems: center; flex: 1; border: 0px; } .roundshape { height: 48; //any of height width: 48; //any of width justifyContent: center; borderRadius: 24px; } .greenButton { background-color: rgba(#6DAA63, .9); } .disabledGreenButton { background-color: rgba(#395936, .9); } .blueButton { background-color: rgba(#4572a6, 1); } .disabledBlueButton { background-color: rgba(#2E4C6F, 1); } .greenButtoniOS { padding-top: 4px; background-color: rgba(#6DAA63, .9); } .disabledGreenButtoniOS { padding-top: 4px; background-color: rgba(#395936, .9); } .blueButtoniOS { padding-top: 4px; background-color: rgba(#4572a6, 1); } .disabledBlueButtoniOS { padding-top: 4px; background-color: rgba(#2E4C6F, 1); } .footer { flex: 1; justify-content: flex-end; padding-bottom: 0px; } .backButton { justifyContent: center; alignItems: center; background-color: red; color: white; border-radius: 5px; border: 1px; } .navigationContainer { } .navigationButtonGroup { justify-content: center; + border: 0px; } .navigationButton { } .navigationButtonSelected { background-color: white; } .qrCodeButton { padding-top: 0px; background-color: white; } diff --git a/app/components/ChatActions.js b/app/components/ChatActions.js index 65fc98e..2317b0d 100644 --- a/app/components/ChatActions.js +++ b/app/components/ChatActions.js @@ -1,146 +1,203 @@ import PropTypes from 'prop-types' import React from 'react' -import Switch from 'react-native' -import { Bubble, MessageText, InputToolbar } from 'react-native-gifted-chat' -import DocumentPicker from 'react-native-document-picker'; -//import * as ImagePicker from 'expo-image-picker'; - - -import { - StyleSheet, - Text, - TouchableOpacity, - View, - ViewPropTypes, -} from 'react-native' - -export default class CustomActions extends React.Component { - onActionsPress = () => { - let options = [ - 'Choose photo from library', - 'Take a picture', - 'Cancel', - ] - - options = [ - 'Upload file...', - 'Cancel', - ] - const cancelButtonIndex = options.length - 1 - this.context.actionSheet().showActionSheetWithOptions( - { - options, - cancelButtonIndex, - }, - async buttonIndex => { - const { onSend } = this.props - switch (buttonIndex) { - case 0: - this.onAttachFile(); - return - case 1: - takePictureAsync(onSend) - return - default: +import autoBind from 'auto-bind'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' +import AudioRecorderPlayer from 'react-native-audio-recorder-player' +import {TouchableOpacity, View, Platform} from 'react-native' +import styles from '../assets/styles/blink/_ContactsListBox.scss'; +const RNFS = require('react-native-fs'); +import AudioRecord from 'react-native-audio-record'; + +const options = { + sampleRate: 16000, // default 44100 + channels: 1, // 1 or 2, default 1 + bitsPerSample: 16, // 8 or 16, default 16 + audioSource: 6, // android only (see below) + wavFile: 'sylk-audio-recording.wav' // default 'audio.wav' +}; + +AudioRecord.init(options); + +class CustomActions extends React.Component { + constructor(props) { + + super(props); + autoBind(this); + + this.state = {recording: false, texting: false, sendingImage: false} + this.timer = null; + this.audioRecorderPlayer = new AudioRecorderPlayer(); + this.ended = false; + } + + componentWillUnmount() { + this.ended = true; + this.stopRecording(); + } + + UNSAFE_componentWillReceiveProps(nextProps) { + this.setState({texting: nextProps.texting, + playing: nextProps.playing, + audioSendFinished: nextProps.audioSendFinished, + sendingImage: nextProps.sendingImage + }); + if (nextProps.audioSendFinished) { + this.deleteAudioRecording() } - }, - ) - } - - onAttachFile = async () => { try { const results = await DocumentPicker.pick({ type: [DocumentPicker.types.allFiles], }); - - console.log(results); - let res = results[0]; - let type = res.name.slice(res.name.lastIndexOf('.') + 1); - this.props.onSendWithFile(res); - /* - if ( - res.type == 'application/pdf' || - res.type == 'image/jpeg' || - res.type == 'image/png' || - res.type == 'image/jpg' - ) { - } else { - alert(`${type} is not allowed. Only images types are supported.`); - } - */ - } catch (err) { - //Handling any exception (If any) - if (DocumentPicker.isCancel(err)) { - //If user canceled the document selection - // alert('Canceled from single doc picker'); - } else { - //For Unknown Error - // alert('Unknown Error: ' + JSON.stringify(err)); - throw err; - } - } - }; + } - renderIcon = () => { - if (this.props.renderIcon) { - return this.props.renderIcon() + onActionsPress = () => { + if (this.state.audioRecording) { + this.setState({audioRecording: false}); + this.props.audioRecorded(null); + + /* + if (this.state.playing) { + this.setState({playing: false}); + this.onStopPlay(); + } else { + this.setState({playing: true}); + this.onStartPlay() + } + */ + } else { + if (this.state.playing) { + this.props.stopPlaying(); + } else if (!this.state.recording) { + this.setState({recording: true}); + this.props.onRecording(true); + console.log('Recording audio start'); + this.onStartRecord(); + this.timer = setTimeout(() => { + this.stopRecording(); + }, 20000); + } else { + this.stopRecording(); + } + } } - return ( - - + - - ) - } - - render() { - return ( - - {this.renderIcon()} - - ) - } -} -const styles = StyleSheet.create({ - container: { - width: 26, - height: 26, - marginLeft: 10, - marginBottom: 10, - }, - wrapper: { - borderRadius: 13, - borderColor: '#b2b2b2', - borderWidth: 2, - flex: 1, - }, - iconText: { - color: '#b2b2b2', - fontWeight: 'bold', - fontSize: 16, - backgroundColor: 'transparent', - textAlign: 'center', - }, -}) - -CustomActions.contextTypes = { - actionSheet: PropTypes.func, -} + stopRecording() { + if (this.timer !== null) { + clearTimeout(this.timer); + this.timer = null; + } + this.setState({recording: false}); + this.props.onRecording(false); + this.onStopRecord(); + } + + renderIcon () { + let color = "green"; + let name = this.state.recording ? "pause" : "microphone"; + + if (this.state.audioRecording) { + name = "delete"; + color = "red" + } + + if (this.state.texting || this.state.sendingImage || this.state.playing) { + return () + } -CustomActions.defaultProps = { - onSend: () => {}, - options: {}, - renderIcon: null, - containerStyle: {}, - wrapperStyle: {}, - iconTextStyle: {}, + return ( + + + + ) + } + + deleteAudioRecording() { + this.setState({audioRecording: null}); + } + + onStartRecord = async () => { + AudioRecord.start(); + + /* bellow code only works on Android + let path = RNFS.DocumentDirectoryPath + "/" + 'sylk-audio-recording.mp4'; + const result = await this.audioRecorderPlayer.startRecorder(path); + this.audioRecorderPlayer.addRecordBackListener((e) => { + this.setState({ + recordSecs: e.currentPosition, + recordTime: this.audioRecorderPlayer.mmssss( + Math.floor(e.currentPosition), + ), + }); + }); + */ + }; + + onStopRecord = async () => { + if (this.ended) { + return; + } + + const result = await AudioRecord.stop(); + this.props.audioRecorded(result); + this.setState({audioRecording: result}); + + /* bellow code only works on Android + + const result = await this.audioRecorderPlayer.stopRecorder(); + this.audioRecorderPlayer.removeRecordBackListener(); + this.setState({recordSecs: 0}); + */ + + this.props.audioRecorded(result); + }; + + async onStartPlay () { + const msg = await this.audioRecorderPlayer.startPlayer(); + this.audioRecorderPlayer.addPlayBackListener((e) => { + this.setState({ + currentPositionSec: e.currentPosition, + currentDurationSec: e.duration, + playTime: this.audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)), + duration: this.audioRecorderPlayer.mmssss(Math.floor(e.duration)), + }); + }); + }; + + onPausePlay = async () => { + await this.audioRecorderPlayer.pausePlayer(); + }; + + onStopPlay = async () => { + console.log('onStopPlay'); + this.audioRecorderPlayer.stopPlayer(); + this.audioRecorderPlayer.removePlayBackListener(); + this.setState({playing: false}); + }; + + render() { + return ( + + + {this.renderIcon()} + + + ) + } } CustomActions.propTypes = { - onSend: PropTypes.func, - onSendWithFile: PropTypes.func, - options: PropTypes.object, - renderIcon: PropTypes.func, - containerStyle: ViewPropTypes.style, - wrapperStyle: ViewPropTypes.style, - iconTextStyle: Text.propTypes.style, + audioRecorded: PropTypes.func, + onRecording: PropTypes.func, + stopPlaying: PropTypes.func, + options: PropTypes.object, + texting: PropTypes.bool, + sendingImage: PropTypes.bool, + audioSendFinished: PropTypes.bool } + +export default CustomActions; diff --git a/app/components/ConferenceBox.js b/app/components/ConferenceBox.js index 39ccc39..5540b5a 100644 --- a/app/components/ConferenceBox.js +++ b/app/components/ConferenceBox.js @@ -1,2764 +1,2849 @@ 'use strict'; import React, {useState, Component, Fragment} from 'react'; import { Clipboard, View, Platform, TouchableWithoutFeedback, TouchableOpacity, Dimensions, SafeAreaView, ScrollView, FlatList, TouchableHighlight, Keyboard, Switch, Animated, PanResponder} from 'react-native'; import PropTypes from 'prop-types'; import * as sylkrtc from 'react-native-sylkrtc'; import classNames from 'classnames'; import debug from 'react-native-debug'; import superagent from 'superagent'; import autoBind from 'auto-bind'; import { RTCView } from 'react-native-webrtc'; import { IconButton, Appbar, Portal, Modal, Surface, Paragraph, Text } from 'react-native-paper'; import uuid from 'react-native-uuid'; import config from '../config'; import utils from '../utils'; //import AudioPlayer from './AudioPlayer'; import ConferenceDrawer from './ConferenceDrawer'; import ConferenceDrawerLog from './ConferenceDrawerLog'; // import ConferenceDrawerFiles from './ConferenceDrawerFiles'; import ConferenceDrawerParticipant from './ConferenceDrawerParticipant'; import ConferenceDrawerParticipantList from './ConferenceDrawerParticipantList'; import ConferenceDrawerSpeakerSelection from './ConferenceDrawerSpeakerSelection'; import ConferenceDrawerSpeakerSelectionWrapper from './ConferenceDrawerSpeakerSelectionWrapper'; import ConferenceHeader from './ConferenceHeader'; import ConferenceCarousel from './ConferenceCarousel'; import ConferenceParticipant from './ConferenceParticipant'; import ConferenceMatrixParticipant from './ConferenceMatrixParticipant'; import ConferenceParticipantSelf from './ConferenceParticipantSelf'; import InviteParticipantsModal from './InviteParticipantsModal'; import ConferenceAudioParticipantList from './ConferenceAudioParticipantList'; import ConferenceAudioParticipant from './ConferenceAudioParticipant'; -import { GiftedChat, Bubble, MessageText } from 'react-native-gifted-chat' +import { GiftedChat, Bubble, MessageText, Send, MessageImage } from 'react-native-gifted-chat' +import {renderBubble } from './ContactsListBox'; +import {launchCamera, launchImageLibrary} from 'react-native-image-picker'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import DocumentPicker from 'react-native-document-picker'; +import RNFetchBlob from "rn-fetch-blob"; +import VideoPlayer from 'react-native-video-player'; + import xss from 'xss'; -import CustomChatActions from './ChatActions'; import * as RNFS from 'react-native-fs'; import styles from '../assets/styles/blink/_ConferenceBox.scss'; import RNBackgroundDownloader from 'react-native-background-downloader'; import md5 from "react-native-md5"; import FileViewer from 'react-native-file-viewer'; import _ from 'lodash'; import { produce } from "immer" -import cloneDeep from 'lodash/cloneDeep'; import moment from 'moment'; import {StatusBar} from 'react-native'; const DEBUG = debug('blinkrtc:ConferenceBox'); debug.enable('*'); function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class ConferenceBox extends Component { constructor(props) { super(props); autoBind(this); this.sliderTimeout = null; this.downloadRequests = {}; this.audioBytesReceived = new Map(); this.audioBandwidth = new Map(); this.bandwidthDownload = 0; this.bandwidthUpload = 0; this.videoBytesReceived = new Map(); this.videoBandwidth = new Map(); this.audioPacketLoss = new Map(); this.videoPacketLoss = new Map(); this.packetLoss = new Map(); this.latency = new Map(); this.mediaLost = new Map(); this.sampleInterval = 1; this.typingTimer = null; let renderMessages = []; if (this.props.remoteUri in this.props.messages) { renderMessages = this.props.messages[this.props.remoteUri]; } this.audioViewMinHeight = 170; let duration = 0; if (this.props.call) { let giftedChatMessage; let direction; duration = Math.floor((new Date() - this.props.callState.startTime) / 1000); this.props.call.messages.forEach((sylkMessage) => { if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } if (sylkMessage.type === 'status') { return; } const existingMessages = renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } direction = sylkMessage.state === 'received' ? 'incoming': 'outgoing'; if (direction === 'incoming' && sylkMessage.sender.uri === this.props.account.id) { direction = 'outgoing'; } giftedChatMessage = utils.sylkToRenderMessage(sylkMessage, null, direction); renderMessages.push(giftedChatMessage); this.saveConferenceMessage(this.props.remoteUri, giftedChatMessage); }); } const videoEnabled = this.props.call && this.props.call.getLocalStreams()[0].getVideoTracks().length > 0; let bottomHeight = Dimensions.get('window').height * 50/100; //console.log('bottomHeight', bottomHeight); let participants = []; if (props.call) { props.call.participants.forEach((p) => { if (!p.timestamp) { p.timestamp = Date.now(); } }); participants = props.call.participants.slice(); } this.state = { callOverlayVisible: true, remoteUri: this.props.remoteUri, call: this.props.call, accountId: this.props.call ? this.props.call.account.id : null, renderMessages: renderMessages, ended: false, duration: duration, isTyping: false, keyboardVisible: false, videoEnabled: videoEnabled, audioMuted: this.props.muted, videoMuted: !this.props.inFocus, videoMutedbyUser: false, messages: this.props.messages, participants: participants, showInviteModal: false, showDrawer: false, keyboardHeight: 0, showFiles: false, shareOverlayVisible: false, showSpeakerSelection: false, activeSpeakers: props.call.activeParticipants.slice(), selfDisplayedLarge: false, eventLog: [], sharedFiles: props.call.sharedFiles.slice(), largeVideoStream: null, previousParticipants: this.props.previousParticipants, inFocus: this.props.inFocus, reconnectingCall: this.props.reconnectingCall, terminated: this.props.terminated, chatView: !videoEnabled, audioView: !videoEnabled, isLandscape: this.props.isLandscape, selectedContacts: this.props.selectedContacts, activeDownloads: {}, offset : 0, topHeight : Dimensions.get('window').height - bottomHeight, bottomHeight : duration > 10 && this.props.conferenceSliderPosition ? this.props.conferenceSliderPosition : bottomHeight, // min height for bottom pane header, deviceHeight : Dimensions.get('window').height, isDividerClicked: false, pan : new Animated.ValueXY() }; this._panResponder = PanResponder.create({ onMoveShouldSetResponderCapture: () => true, onMoveShouldSetPanResponderCapture: () => true, // Initially, set the Y position offset when touch start onPanResponderGrant: (e, gestureState) => { this.setState({ offset: e.nativeEvent.pageY, isDividerClicked: true }) this.sliderTimeout = setTimeout(() => { this.setState({ isDividerClicked: false }) }, 2000); }, // When we drag the divider, set the bottomHeight (component state) again. onPanResponderMove: (e, gestureState) => { //let b = gestureState.moveY > (this.state.deviceHeight - 40) ? 40 : this.state.deviceHeight - gestureState.moveY - 40; const maxH = Dimensions.get('window').height - this.audioViewMinHeight - 110; let b = Math.floor(this.state.deviceHeight - gestureState.moveY); if (b > maxH) { b = maxH; } var d = this.state.bottomHeight - b; if (d < 0) { d = -d; } if (d >= 30) { this.setState({ bottomHeight : b, offset: e.nativeEvent.pageY, isDividerClicked: true }) if (this.sliderTimeout) { clearTimeout(this.sliderTimeout); this.sliderTimeout = null; } this.sliderTimeout = setTimeout(() => { console.log('Turn slider off'); this.setState({ isDividerClicked: false }) this.sliderTimeout = null; }, 2000); this.props.saveSliderFunc(b); } }, onPanResponderRelease: (e, gestureState) => { // Do something here for the touch end event this.setState({ offset: e.nativeEvent.pageY, }) } }); const friendlyName = this.state.remoteUri.split('@')[0]; //if (window.location.origin.startsWith('file://')) { this.conferenceUrl = `${config.publicUrl}/conference/${friendlyName}`; //} else { // this.conferenceUrl = `${window.location.origin}/conference/${friendlyName}`; //} const emailMessage = `You can join me in the conference using a Web browser at ${this.conferenceUrl} ` + 'or by using the freely available Sylk WebRTC client app at http://sylkserver.com'; const subject = 'Join me, maybe?'; this.emailLink = `mailto:?subject=${encodeURI(subject)}&body=${encodeURI(emailMessage)}`; this.overlayTimer = null; this.logEvent = {}; this.uploads = []; this.selectSpeaker = 1; this.foundContacts = new Map(); if (this.props.call) { this.lookupContact(this.props.call.localIdentity._uri, this.props.call.localIdentity._displayName); } [ 'error', 'warning', 'info', 'debug' ].forEach((level) => { this.logEvent[level] = ( (action, messages, originator) => { const log = this.state.eventLog.slice(); log.unshift({originator, originator, level: level, action: action, messages: messages}); this.setState({eventLog: log}); } ); }); this.invitedParticipants = new Map(); // TODO preserve this list between route changes console.log('Initial call duration', duration); props.initialParticipants.forEach((uri) => { const existing_participants = participants.filter(p => p.identity._uri === uri); if (existing_participants.length === 0) { this.invitedParticipants.set(uri, {timestamp: Date.now(), status: duration < 10 ? 'Invited' : 'No answer'}) this.lookupContact(uri); } }); this.participantsTimer = setInterval(() => { this.updateParticipantsStatus(); }, this.sampleInterval * 1000); + this.props.getMessages(this.state.remoteUri.split('@')[0]); + setTimeout(() => { this.listSharedFiles(); }, 1000); } get chatViewHeight() { const wh = Dimensions.get('window').height; const kh = this.state.keyboardHeight; const sh = (Platform.OS === 'android') ? StatusBar.currentHeight : 0; //console.log('window height', Math.floor(wh)); //console.log('keyboa height', Math.floor(kh)); //console.log('status height', Math.floor(sh)); let ah = Platform.OS === 'android' ? wh - kh - sh - 30: wh - 50; //console.log('Available height', Math.floor(ah)); return ah; } messageExists(giftedChatMessage, sylkMessage) { if (sylkMessage._id === giftedChatMessage._id) { return true; } let gs_timestamp = giftedChatMessage.createdAt; let sylk_timestamp = sylkMessage.timestamp; gs_timestamp.setMilliseconds(0); sylk_timestamp.setMilliseconds(0); if (gs_timestamp.toString() === sylk_timestamp.toString() && giftedChatMessage.text === sylkMessage.content) { return true; } return false; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.hasOwnProperty('isDividerClicked')) { this.setState({isDividerClicked: nextProps.isDividerClicked}); } if (nextProps.hasOwnProperty('keyboardVisible')) { this.setState({keyboardVisible: nextProps.keyboardVisible}); } if (nextProps.call !== null && nextProps.call !== this.state.call) { this.setState({call: nextProps.call}); } if (nextProps.inFocus !== this.state.inFocus) { if (nextProps.inFocus) { if (!this.state.videoMutedbyUser) { this._resumeVideo(); } } else { this._muteVideo(); } this.setState({inFocus: nextProps.inFocus}); } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } let renderMessages = []; if (nextProps.remoteUri in nextProps.messages) { nextProps.messages[nextProps.remoteUri].forEach((message) => { const existingMessages = this.state.renderMessages.filter(msg => msg._id === message._id); if (existingMessages.length > 0) { return; } renderMessages.push(message); }); if (nextProps.call) { this.setState({sharedFiles: nextProps.call.sharedFiles.slice()}); let giftedChatMessage; let existingMessages; let previousMessages; nextProps.call.messages.forEach((sylkMessage) => { if (sylkMessage.type === 'status') { return; } if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } existingMessages = renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } existingMessages = this.state.renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } let direction = sylkMessage.state === 'received' ? 'incoming': 'outgoing'; if (direction === 'incoming' && sylkMessage.sender.uri === this.props.account.id) { direction = 'outgoing'; } giftedChatMessage = utils.sylkToRenderMessage(sylkMessage, null, direction); renderMessages.push(giftedChatMessage); this.saveConferenceMessage(this.props.remoteUri, giftedChatMessage); }); } } if (nextProps.bottomHeight) { this.setState({ topHeight : nextProps.keyboardVisible === false ? nextProps.topHeight : 0, // min height for top pane heade bottomHeight : nextProps.bottomHeight, // min height for bottom pane header, }); } this.setState({terminated: nextProps.terminated, remoteUri: nextProps.remoteUri, renderMessages: GiftedChat.append(this.state.renderMessages, renderMessages), isLandscape: nextProps.isLandscape, messages: nextProps.messages, offset: nextProps.offset, activeDownloads: nextProps.activeDownloads, accountId: !this.state.accountId && nextProps.call ? this.props.call.account.id : this.state.accountId, selectedContacts: nextProps.selectedContacts}); } getInfo() { let info; let bandwidthDownload = this.bandwidthDownload; let bandwidthUpload = this.bandwidthUpload; let unit = 'Kbit/s'; if (this.bandwidthDownload > 0 && this.bandwidthUpload > 0) { if (this.bandwidthDownload > 1100 || this.bandwidthUpload > 1100) { bandwidthDownload = Math.ceil(this.bandwidthDownload / 1000 * 100) / 100; bandwidthUpload = Math.ceil(this.bandwidthUpload / 1000 * 100) / 100; unit = 'Mbit/s'; } info = '⇣' + bandwidthDownload + ' ⇡' + bandwidthUpload + ' ' + unit; } return info; } saveConferenceMessage(uri, message) { this.props.saveConferenceMessage(uri, message); } updateConferenceMessage(uri, message) { this.props.updateConferenceMessage(uri, message); } onSendFromUser() { console.log('On send from user...'); } uploadBegin(response) { var jobId = response.jobId; console.log('UPLOAD HAS BEGUN! JobId: ' + jobId); }; uploadProgress(response) { var percentage = Math.floor((response.totalBytesSent/response.totalBytesExpectedToSend) * 100); console.log('UPLOAD IS ' + percentage + '% DONE!'); }; transferComplete(evt) { console.log("Upload has finished", evt); } transferFailed(evt) { console.log("An error occurred while transferring the file.", evt); } transferCanceled(evt) { console.log("The transfer has been canceled by the user."); } filePath(filename) { let dir = RNFS.DocumentDirectoryPath + '/conference/' + this.state.remoteUri + '/files'; let path; RNFS.mkdir(dir); path = dir + '/' + filename.toLowerCase(); return path; } tsize(fsize) { let size = fsize + + " B"; if (fsize > 1024 * 1024) { size = Math.ceil(fsize/1024/1024) + " MB"; } else if (fsize < 1024 * 1024) { size = Math.ceil(fsize/1024) + " KB"; } return size; } toggleDownload(metadata) { //console.log('toggleDownload', metadata); let renderMessages = this.state.renderMessages; let newRenderMessages = []; renderMessages.forEach((msg) => { - if (msg._id === metadata.msg_id) { + if (msg._id === metadata.transfer_id) { //console.log('Found message', msg.metadata); if (msg.metadata.progress === null) { msg.metadata.progress = 0; msg.metadata.failed = false; //console.log('Start metadata', msg.metadata); this.downloadFile(metadata); } else { //console.log('Stop metadata', msg.metadata); this.stopDownloadFile(metadata); msg.metadata.progress = null; } this.updateConferenceMessage(this.props.remoteUri, msg); } }); } - renderCustomActions = props => - ( - + async _launchCamera() { + let options = {saveToPhotos: true, + mediaType: 'photo', + maxWidth: 2000, + cameraType: 'front' + } + await launchCamera(options, this.cameraCallback); + } + + async _launchImageLibrary() { + let options = {}; + await launchImageLibrary(options, this.cameraCallback); + } + + cameraCallback (result) { + if (result.assets) { + this.uploadFile(result.assets[0]); + } + } + + async _pickDocument() { + try { + const result = await DocumentPicker.pick({ + type: [DocumentPicker.types.allFiles], + copyTo: 'documentDirectory', + mode: 'import', + allowMultiSelection: false, + }); + + const fileUri = result[0].fileCopyUri; + if (!fileUri) { + console.log('File URI is undefined or null'); + return; + } + + console.log('Send file', fileUri); + + this.uploadfile(fileUri); + + } catch (err) { + if (DocumentPicker.isCancel(err)) { + console.log('User cancelled file picker'); + } else { + console.log('DocumentPicker err => ', err); + throw err; + } + } + }; + + renderSend = (props) => { + return ( + + + + + + + + + + + + ); + }; + + renderMessageImage =(props) => { + return ( + ) + } + + renderMessageVideo(props){ + const { currentMessage } = props; + + return ( + + + + ); + }; renderCustomView(props) { const {currentMessage} = props; const { text: currText } = currentMessage; if (!currentMessage.metadata) { return null; } let status = ''; let label = 'Uploading...'; let showSwitch = currentMessage.download || (currentMessage.url && (currentMessage.metadata.progress || !currentMessage.metadata.progress !== 100) && !currentMessage.local_url && !utils.isImage(currentMessage.metadata.name)) ; let switchOn = (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) ? true : false; if (currentMessage.direction === 'incoming') { label = 'Downloading...'; if (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) { status = currentMessage.label + ' - ' + currentMessage.metadata.progress + '%'; } else { if (!utils.isImage(currentMessage.metadata.name)) { status = 'Swipe to download \n' + currentMessage.label; } else { status = currentMessage.label; } } } else { if (!currentMessage.local_url && currentMessage.metadata.progress === null) { switchOn = false; } if (currentMessage.metadata.progress || currentMessage.metadata.progress === 0) { status = currentMessage.label + ' - ' + currentMessage.metadata.progress + '%'; } else { status = currentMessage.label; } } if (currentMessage.url && !currentMessage.local_url) { //console.log('--- Render message', currentMessage.metadata.name, currentMessage.metadata.progress); } if (!utils.isImage(currentMessage.metadata.name) && !currentMessage.local_url) { //console.log('Show switch', currentMessage._id, currentMessage.metadata.name, switchOn, currentMessage.metadata.progress); } //console.log('text =', currentMessage.text, 'label =', label, 'status =', status); let progress = 'Download'; if (currentMessage.metadata.progress !== null) { progress = currentMessage.metadata.progress + ' %'; } if (showSwitch) { return ( {progress} this.toggleDownload(currentMessage.metadata)}/> ); } else { return null; } }; - renderMessageBubble (props) { - let rightColor = '#0084ff'; - let leftColor = '#f0f0f0'; - - if (props.currentMessage.failed) { - rightColor = 'red'; - leftColor = 'red'; - } else { - if (props.currentMessage.pinned) { - rightColor = '#2ecc71'; - leftColor = '#2ecc71'; - } - } - - return ( - - ) - } - failedFileUploadMessage(id) { let renderMessages = this.state.renderMessages; let newRenderMessages = []; renderMessages.forEach((msg) => { if (msg._id === id) { msg.sent = true; msg.received = false; msg.failed = true; msg.metadata.progress = null; msg.metadata.started = false; } newRenderMessages.push(msg); this.updateConferenceMessage(this.state.remoteUri, msg); }); } - async uploadFile(file) { - console.log('Uploading file', file); - - let metadata = { - name: file.name, - type: file.type, - size: file.size, - uri: file.uri - }; - - metadata.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + this.props.call.id + '/' + file.name;; - metadata.msg_id = md5.hex_md5(this.state.remoteUri + '_' + file.name); - metadata.local_url = this.filePath(metadata.name); - metadata.progress = 0; + async uploadFile(fileObject) { + console.log('Uploading file', fileObject); + + var id = md5.hex_md5(this.state.remoteUri + '_' + basename); + let filepath = fileObject.uri ? fileObject.uri : fileObject; + const basename = filepath.split('\\').pop().split('/').pop(); + let stats_filename = filepath.startsWith('file://') ? filepath.substr(7, filepath.length - 1) : filepath; + const { size } = await RNFetchBlob.fs.stat(stats_filename); + + let file_transfer = { 'path': filepath, + 'filename': basename, + 'filesize': fileObject.fileSize || size, + 'sender': {'uri': this.state.accountId}, + 'receiver': {'uri': this.state.remoteUri}, + 'transfer_id': id, + 'direction': 'outgoing' + }; + + if (fileObject.filetype) { + file_transfer.filetype = fileObject.filetype; + } + + let text = utils.beautyFileNameForBubble(file_transfer); + + let msg = { + _id: id, + key: id, + text: text, + metadata: file_transfer, + received: false, + sent: false, + pending: true, + createdAt: new Date(), + direction: 'outgoing', + user: {} + } - if (metadata.size > 1024 * 1024 * 40) { - this.postChatSystemMessage(metadata.name + 'is too big', false); - return; + if (utils.isImage(basename)) { + msg.image = filepath; + } else if (utils.isAudio(basename)) { + msg.audio = filepath; + } else if (utils.isVideo(basename)) { + msg.video = filepath; } - RNFS.readFile(metadata.uri, 'base64').then(res => { - let image; - let isImage = utils.isImage(metadata.name); - let label = metadata.name.toLowerCase(); + this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); - if (isImage) { - image = metadata.uri; - RNFS.copyFile(metadata.uri, metadata.local_url).then((success) => { - console.log('Copy file to', metadata.local_url); - image = 'file://' + metadata.local_url; - }).catch((err) => { - console.log('Error writing to file', metadata.local_url, err.message); - }); - label = this.tsize(metadata.size); - } else { - label = label + " (" + this.tsize(metadata.size) + ")"; - } + file_transfer.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + this.props.call.id + '/' + basename; + file_transfer.transfer_id = id; + let localPath = this.filePath(basename); + await RNFS.copyFile(file_transfer.path, localPath); + //console.log('Copy file to', localPath); + file_transfer.local_url = localPath; + file_transfer.progress = 0; + msg.metadata = file_transfer; - const giftedChatMessage = { - _id: metadata.msg_id, - key: metadata.msg_id, - createdAt: new Date(), - text: label, - metadata: metadata, - received: false, - sent: false, - pending: true, - label: label, - direction: 'outgoing', - image: image, - user: {} - }; - - //console.log('metadata', metadata); - - this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); - this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); + RNFS.readFile(localPath, 'base64').then(res => { + this.saveConferenceMessage(this.state.remoteUri, msg); + this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); var oReq = new XMLHttpRequest(); oReq.addEventListener("load", this.transferComplete); oReq.addEventListener("error", this.transferFailed); oReq.addEventListener("abort", this.transferCanceled); - oReq.open('POST', metadata.url); + oReq.open('POST', file_transfer.url); const formData = new FormData(); formData.append(res); oReq.send(formData); if (oReq.upload) { oReq.upload.onprogress = ({ total, loaded }) => { const progress = Math.ceil(loaded / total * 100); - this.updateFileMessage(metadata.msg_id, progress); + this.updateFileMessage(id, progress); }; } }) .catch(err => { - console.log(err.message, err.code); + console.log('Failed to upload file', err.message, err.code); }); } updateFileMessage(id, progress, failed=false) { //make a change togglePlay(msgidx) { console.log('Update file progress', id, progress); let renderMessages = this.state.renderMessages; let newRenderMessages = []; let nextState; renderMessages.forEach((msg) => { if (msg._id === id) { - console.log('Update file', id, msg.metadata.name, progress, failed); + //console.log('Update file transfer for msg', msg); if (failed) { msg.failed = true; msg.sent = true; msg.pending = false; msg.received = false; msg.metadata.progress = null; - console.log('Message failed'); this.postChatSystemMessage('Download failed', false); this.updateConferenceMessage(this.state.remoteUri, msg); - return; } msg.metadata.progress = progress; if (progress !== null) { msg.failed = false; msg.received = null; } if (progress === 100 && (!msg.sent || !msg.received)) { - msg.local_url = msg.metadata.local_url; - msg.url = msg.metadata.url; - msg.metadata.progress = null; msg.failed = false; msg.pending = false; msg.sent = msg.direction === 'outgoing' ? true : false; msg.received = true; - - if (msg.image || utils.isImage(msg.metadata.name)) { - msg.text = 'Image of ' + this.tsize(msg.metadata.size); - msg.image = 'file://' + msg.metadata.local_url; - } else { - msg.text = msg.metadata.name + " (" + this.tsize(msg.metadata.size) + ")"; - } - - console.log(msg.metadata.name, msg.direction === 'outgoing' ? 'Upload completed' : 'Download completed'); + msg.text = utils.beautyFileNameForBubble(msg.metadata); + console.log(msg.metadata.filename, msg.direction === 'outgoing' ? 'Upload completed' : 'Download completed'); //console.log('Update metadata', msg.metadata); - this.updateConferenceMessage(this.state.remoteUri, msg); } - newRenderMessages.push(cloneDeep(msg)); - } else { - newRenderMessages.push(msg); } + newRenderMessages.push(msg); }); - this.setState({ renderMessages: newRenderMessages}); + this.setState({renderMessages: GiftedChat.append(newRenderMessages, [])}); } purgeSharedFiles() { this.state.renderMessages.forEach((msg) => { if (msg.url) { if (!msg.image && !msg.local_url) { const parts = msg.url.split('/'); const filename = parts[parts.length - 1]; let existingFiles = this.state.sharedFiles.filter(file => md5.hex_md5(this.state.remoteUri + '_' + filename) === msg._id); if (existingFiles.length === 0) { this.props.deleteConferenceMessage(this.state.remoteUri, msg); } } } }); } async listSharedFiles() { - let url; - let i = 0; - let image; + console.log('--- List shared files'); - //console.log('--- List shared files'); + let messages = this.state.renderMessages; + let new_messages = []; + let found = false; + let exists = false; - this.state.sharedFiles.forEach((file)=>{ + for (const file of this.state.sharedFiles) { if (file.session === this.props.call.id) { // skip my own files - return; + continue; } let metadata = {}; - - metadata.size = file.filesize; - metadata.name = file.filename; - metadata.uploader = file.uploader; - metadata.session = file.session; - console.log('--- Shared file:', metadata.uploader.uri, metadata.name); - - image = null; - url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + metadata.session + '/' + metadata.name; - metadata.url = url; - metadata.msg_id = md5.hex_md5(this.state.remoteUri + '_' + metadata.name); - - let label = metadata.name.toLowerCase(); - label = label + " (" + this.tsize(metadata.size) + ")"; - - const existingMessages = this.state.renderMessages.filter(msg => msg._id === metadata.msg_id); - if (existingMessages.length > 0) { - //console.log('Message already exists', metadata.msg_id ); - return; + let text; + let url; + let msg; + found = false; + exists = false; + + metadata.transfer_id = md5.hex_md5(this.state.remoteUri + '_' + file.filename); + + for (const msg of messages) { + if (msg._id === metadata.transfer_id) { + found = true; + metadata = msg.metadata; + console.log('File transfer', metadata.filename, 'already exists'); + msg.text = utils.beautyFileNameForBubble(metadata); + exists = await RNFS.exists(metadata.local_url); + if (exists) { + console.log('Local file', metadata.filename, 'already exists'); + metadata.received = true; + if (utils.isImage(metadata.filename)) { + msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isAudio(metadata.filename)) { + msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isVideo(metadata.filename)) { + msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } + } else { + metadata.received = false; + msg.image = null; + msg.audio = null; + msg.video = null; + } + console.log('Updated message', msg); + new_messages.push(msg); + } } - const direction = metadata.uploader.uri === this.props.account.id ? 'outgoing' : 'incoming'; - - let text = metadata.name.toLowerCase(); - let isImage = utils.isImage(metadata.name); - - if (isImage) { - text = 'Image of '; + if (found) { + this.setState({renderMessages: GiftedChat.append(new_messages, [])}); + console.log('Update list and return'); + return; } - text = text + " (" + this.tsize(metadata.size) + ")"; - metadata.local_url = this.filePath(metadata.name); - - RNFS.exists(metadata.local_url).then(res => { - if (res) { - //console.log('File', file.name, 'already exists'); - if (isImage) { - image = 'file://' + metadata.local_url; - } - - const giftedChatMessage = { - _id: metadata.msg_id, - key: metadata.msg_id, - createdAt: new Date(), - text: text, - url: url, - local_url: metadata.local_url, - metadata: metadata, - image: image, - received: false, - failed: false, - sent: false, - user: direction === 'incoming' ? {_id: metadata.uploader.uri, name: metadata.uploader.displayName || metadata.uploader.uri} : {} - }; - - this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); - this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); - - } else { - metadata.progress = isImage ? 0 : null; - - const giftedChatMessage = { - _id: metadata.msg_id, - key: metadata.msg_id, - createdAt: new Date(), - text: text, - url: url, - metadata: metadata, - label: label, - failed: false, - received: false, - sent: false, - direction: direction, - user: direction === 'incoming' ? {_id: metadata.uploader.uri, name: metadata.uploader.displayName || metadata.uploader.uri} : {} - }; - - this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); - this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); + metadata.filesize = file.filesize; + metadata.filename = file.filename; + metadata.sender = {uri: file.uploader.uri}; + metadata.receiver = {uri: this.state.remoteUri}; + metadata.session = file.session; + metadata.url = this.props.fileSharingUrl + '/' + this.state.remoteUri + '/' + metadata.session + '/' + metadata.name; + metadata.direction = metadata.sender.uri === this.props.account.id ? 'outgoing' : 'incoming'; + metadata.local_url = this.filePath(metadata.filename); + + console.log('--- Shared file:', metadata); + + text = utils.beautyFileNameForBubble(metadata); + + msg = { + _id: metadata.transfer_id, + key: metadata.transfer_id, + createdAt: new Date(), + text: text, + url: url, + metadata: metadata, + received: false, + failed: false, + sent: false, + user: metadata.direction === 'incoming' ? {_id: metadata.sender.uri, name: metadata.sender.displayName || metadata.sender.uri} : {} + }; - if (isImage) { - this.downloadFile(metadata); - } + exists = await RNFS.exists(metadata.local_url); + if (exists) { + console.log('Local file new', metadata.local_url, 'already exists'); + metadata.received = true; + + if (utils.isImage(metadata.filename)) { + msg.image = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isAudio(metadata.filename)) { + msg.audio = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; + } else if (utils.isVideo(metadata.filename)) { + msg.video = Platform.OS === "android" ? 'file://'+ metadata.local_url : metadata.local_url; } - }); - i = i + 1; - }); + } else { + metadata.progress = 0; + if (isImage) { + this.downloadFile(metadata); + } + } + this.saveConferenceMessage(this.state.remoteUri, msg); + console.log('Adding message for file transfer', msg); + this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg])}); + } setTimeout(() => { this.purgeSharedFiles(); }, 1000); } async stopDownloadFile(metadata) { let renderMessages = this.state.renderMessages; renderMessages.forEach((msg) => { - if (msg._id === metadata.msg_id) { + if (msg._id === metadata.transfer_id) { msg.metadata.progress = null; this.updateConferenceMessage(this.state.remoteUri, msg); } }); - if (metadata.msg_id in this.downloadRequests) { + if (metadata.transfer_id in this.downloadRequests) { console.log('Stop download', metadata.url); - let task = this.downloadRequests[metadata.msg_id]; + let task = this.downloadRequests[metadata.transfer_id]; task.stop(); - delete this.downloadRequests[metadata.msg_id]; + delete this.downloadRequests[metadata.transfer_id]; } } async downloadFile(metadata) { + //console.log('downloadFile', metadata); let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); /* TODO: server needs support for this resume - if (metadata.msg_id in this.downloadRequests) { - let task = this.downloadRequests[metadata.msg_id]; + if (metadata.transfer_id in this.downloadRequests) { + let task = this.downloadRequests[metadata.transfer_id]; console.log('Resume download', metadata.url); task.resume(); return; } */ - const existingTask = lostTasks.filter(task => task.id === metadata.msg_id); + const existingTask = lostTasks.filter(task => task.id === metadata.transfer_id); if (existingTask.length === 1) { var task = existingTask[0]; console.log('Found existing download task', task); task.progress((percent) => { const progress = Math.ceil(percent * 100); - this.updateFileMessage(metadata.msg_id, progress); + this.updateFileMessage(metadata.transfer_id, progress); }).begin((expectedBytes) => { - this.updateFileMessage(metadata.msg_id, 0); + this.updateFileMessage(metadata.transfer_id, 0); }).done(() => { - this.updateFileMessage(metadata.msg_id, 100); + this.updateFileMessage(metadata.transfer_id, 100); }).error((error) => { - this.updateFileMessage(metadata.msg_id, 0, error); + this.updateFileMessage(metadata.transfer_id, 0, error); console.log(task.url, 'download error:', error); }); } else { console.log('Start new download:', metadata.url); - this.updateFileMessage(metadata.msg_id, 0); - this.downloadRequests[metadata.msg_id] = RNBackgroundDownloader.download({ - id: metadata.msg_id, + this.updateFileMessage(metadata.transfer_id, 0); + this.downloadRequests[metadata.transfer_id] = RNBackgroundDownloader.download({ + id: metadata.transfer_id, url: metadata.url, destination: metadata.local_url }).begin((expectedBytes) => { - this.updateFileMessage(metadata.msg_id, 0); + this.updateFileMessage(metadata.transfer_id, 0); console.log(metadata.name, 'will download', expectedBytes, 'bytes'); }).progress((percent) => { const progress = Math.ceil(percent * 100); - this.updateFileMessage(metadata.msg_id, progress); + this.updateFileMessage(metadata.transfer_id, progress); }).done(() => { - this.updateFileMessage(metadata.msg_id, 100); - delete this.downloadRequests[metadata.msg_id]; + this.updateFileMessage(metadata.transfer_id, 100); + delete this.downloadRequests[metadata.transfer_id]; }).error((error) => { console.log(metadata.name, 'download error:', error); - this.updateFileMessage(metadata.msg_id, 0, error); - delete this.downloadRequests[metadata.msg_id]; + this.updateFileMessage(metadata.transfer_id, 0, error); + delete this.downloadRequests[metadata.transfer_id]; }); } } onLongMessagePress(context, currentMessage) { if (currentMessage && currentMessage.text) { let options = [] options.push('Copy'); if (currentMessage.local_url) { options.push('Open'); } options.push('Cancel'); //console.log('currentMessage', currentMessage); let l = options.length - 1; context.actionSheet().showActionSheetWithOptions({options, l}, (buttonIndex) => { let action = options[buttonIndex]; if (action === 'Copy') { Clipboard.setString(currentMessage.text); } else if (action === 'Open') { FileViewer.open(currentMessage.local_url, { showOpenWithDialog: true }) .then(() => { // success }) .catch(error => { // error }); } }); } }; removeInvitedParticipant(uri) { if (this.invitedParticipants.has(uri) > 0) { this.invitedParticipants.delete(uri); this.forceUpdate(); } } updateParticipantsStatus() { let participants_uris = []; this.state.participants.forEach((p) => { participants_uris.push(p.identity._uri); }); this.getConnectionStats(); const invitedParties = Array.from(this.invitedParticipants.keys()); //console.log('Invited participants', invitedParties); //console.log('Current participants', participants_uris); let p; let interval; invitedParties.forEach((_uri) => { if (participants_uris.indexOf(_uri) > 0) { this.invitedParticipants.delete(_uri); } p = this.invitedParticipants.get(_uri); if (!p) { return; } interval = Math.floor((Date.now() - p.timestamp) / 1000); if (p.status == 'No answer' && interval >= 15) { //this.invitedParticipants.delete(_uri); //console.log('Update status', _uri, p.status); p.status = 'reinvite'; interval = 0; } if (p.status.indexOf('Invited') > -1 && interval > 5) { //console.log('Update status', _uri, p.status); p.status = 'Wait .'; } if (p.status.indexOf('.') > -1) { if (interval > 10) { //console.log('Update status', _uri, p.status); p.status = 'No answer'; this.postChatSystemMessage(_uri + ' did not answer', false); } else { //console.log('Update status', _uri, p.status); p.status = p.status + '.'; } } }); this.forceUpdate(); } postChatSystemMessage(text, save=true) { var now = new Date(); var hours = now.getHours(); var mins = now.getMinutes(); var secs = now.getSeconds(); var ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; mins = mins < 10 ? '0' + mins : mins; secs = secs < 10 ? '0' + secs : secs; text = text + ' at ' + hours + ":" + mins + ':' + secs + ' ' + ampm; var id = uuid.v4(); const giftedChatMessage = { _id: uuid.v4(), key: id, createdAt: now, text: text, system: true, }; this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); if (save) { this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); } } componentDidMount() { for (let p of this.state.participants) { p.on('stateChanged', this.onParticipantStateChanged); p.attach(); } this.keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', this._keyboardDidShow ); this.keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', this._keyboardDidHide ); this.props.call.on('participantJoined', this.onParticipantJoined); this.props.call.on('participantLeft', this.onParticipantLeft); this.props.call.on('roomConfigured', this.onConfigureRoom); this.props.call.on('fileSharing', this.onFileSharing); this.props.call.on('composingIndication', this.composingIndicationReceived); this.props.call.on('message', this.messageReceived); this.armOverlayTimer(); // attach to ourselves first if there are no other participants if (this.state.participants.length === 0) { setTimeout(() => { const item = { stream: this.props.call.getLocalStreams()[0], identity: this.props.call.localIdentity }; this.selectVideo(item); }); } else { this.state.participants.forEach((p) => { if (p.identity._uri.search('guest.') === -1 && p.identity._uri !== this.props.call.localIdentity._uri) { // used for history item this.props.saveParticipant(this.props.call.id, this.state.remoteUri, p.identity._uri); this.lookupContact(p.identity._uri, p.identity._displayName); } }); // this.changeResolution(); } if (this.state.videoMuted) { this._muteVideo(); } //let msg = "Others can join the conference using a web browser at " + this.conferenceUrl; //this.postChatSystemMessage(msg, false); if (this.state.selectedContacts) { this.inviteParticipants(this.state.selectedContacts); } } componentWillUnmount() { clearTimeout(this.overlayTimer); clearTimeout(this.participantsTimer); this.uploads.forEach((upload) => { this.props.notificationCenter().removeNotification(upload[1]); upload[0].abort(); }) this.keyboardDidShowListener.remove(); this.keyboardDidHideListener.remove(); } _keyboardDidShow(e) { this.setState({keyboardVisible: true, keyboardHeight: e.endCoordinates.height}); } _keyboardDidHide() { this.setState({keyboardVisible: false, keyboardHeight: 0}); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } composingIndicationReceived(data) { if (this.typingTimer) { clearTimeout(this.typingTimer); } this.setState({isTyping: true}); this.typingTimer = setTimeout(() => { this.setState({isTyping: false}); this.typingTimer = null; }, 5000); } messageReceived(sylkMessage) { //console.log('Conference got message', sylkMessage); if (sylkMessage.sender.uri.indexOf('@conference.') && sylkMessage.content.indexOf('Welcome!') > -1) { return; } const existingMessages = this.state.renderMessages.filter(msg => this.messageExists(msg, sylkMessage)); if (existingMessages.length > 0) { return; } if (sylkMessage.direction === 'incoming' && sylkMessage.sender.uri === this.state.accountId) { sylkMessage.direction = 'outgoing'; } const giftedChatMessage = utils.sylkToRenderMessage(sylkMessage); if (sylkMessage.type === 'status') { return; } this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); this.saveConferenceMessage(this.state.remoteUri, giftedChatMessage); } onSendMessage(messages) { if (!this.props.call) { return; } messages.forEach((message) => { this.props.sendConferenceMessage(message); }); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, messages)}); } lookupContact(uri, displayName) { let photo; let username = uri.split('@')[0]; if (this.props.myContacts.hasOwnProperty(uri) && this.props.myContacts[uri].name) { displayName = this.props.myContacts[uri].name; } else if (this.props.contacts) { let username = uri.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', uri); } if (contact_obj) { displayName = contact_obj.displayName; photo = contact_obj.photo; if (isPhoneNumber) { uri = username; } } else { if (isPhoneNumber) { uri = username; displayName = toTitleCase(username); } } } const c = {photo: photo, displayName: displayName || toTitleCase(username)}; this.foundContacts.set(uri, c) } getConnectionStats() { let audioPackets = 0; let videoPackets = 0; let delay = 0; let audioPacketsLost = 0; let videoPacketsLost = 0; let audioPacketLoss = 0; let videoPacketLoss = 0; let totalPackets = 0; let totalPacketsLost = 0; let totalPacketLoss = 0; let totalAudioBandwidth = 0; let totalVideoBandwidth = 0; let totalSpeed = 0; let bandwidthUpload = 0; let mediaType; if (this.state.participants.length === 0) { this.bandwidthDownload = 0; this.videoBandwidth.set('total', 0); this.audioBandwidth.set('total', 0); } let participants = this.state.participants.concat(this.props.call); participants.forEach((p) => { if (!p._pc) { return; } let identity; if (p.identity) { identity = p.identity.uri; } else { identity = 'myself'; } p._pc.getStats(null).then(stats => { audioPackets = 0; videoPackets = 0; audioPacketsLost = 0; videoPacketsLost = 0; audioPacketLoss = 0; videoPacketLoss = 0; stats.forEach(report => { if (report.type === "ssrc") { report.values.forEach(object => { if (object.mediaType) { mediaType = object.mediaType; } }); report.values.forEach(object => { if (object.bytesReceived && identity !== 'myself') { const bytesReceived = Math.floor(object.bytesReceived); if (mediaType === 'audio') { if (this.audioBytesReceived.has(p.id)) { const lastBytes = this.audioBytesReceived.get(p.id); const diff = bytesReceived - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); totalAudioBandwidth = totalAudioBandwidth + speed; totalSpeed = totalSpeed + speed; //console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity); this.audioBandwidth.set(p.id, speed); } this.audioBytesReceived.set(p.id, bytesReceived); } else if (mediaType === 'video') { if (this.videoBytesReceived.has(p.id)) { const lastBytes = this.videoBytesReceived.get(p.id); const diff = bytesReceived - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); totalVideoBandwidth = totalVideoBandwidth + speed; totalSpeed = totalSpeed + speed; //console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity); this.videoBandwidth.set(p.id, speed); } this.videoBytesReceived.set(p.id, bytesReceived); } } else if (object.bytesSent && identity === 'myself') { const bytesSent = Math.floor(object.bytesSent); if (mediaType === 'audio') { if (this.audioBytesReceived.has(p.id)) { const lastBytes = this.audioBytesReceived.get(p.id); const diff = bytesSent - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log(identity, 'audio bandwidth', speed, 'kbit/s from', identity); this.audioBandwidth.set(p.id, speed); } this.audioBytesReceived.set(p.id, bytesSent); } else if (mediaType === 'video') { if (this.videoBytesReceived.has(p.id)) { const lastBytes = this.videoBytesReceived.get(p.id); const diff = bytesSent - lastBytes; const speed = Math.floor(diff / this.sampleInterval * 8 / 1000); bandwidthUpload = bandwidthUpload + speed; //console.log(identity, 'video bandwidth', speed, 'kbit/s from', identity); this.videoBandwidth.set(p.id, speed); } this.videoBytesReceived.set(p.id, bytesSent); } } else if (object.totalAudioEnergy) { //console.log('Total audio energy', object.totalAudioEnergy, 'from', identity); } else if (object.audioOutputLevel) { //console.log('Output level', object.audioOutputLevel, 'from', identity); this.mediaLost.set(p.id, Math.floor(object.audioOutputLevel) < 5 ? true : false); } else if (object.audioInputLevel) { //console.log('Input level', object.audioInputLevel, 'from', identity); this.mediaLost.set(p.id, Math.floor(object.audioInputLevel) < 5 ? true : false); } else if (object.packetsLost) { totalPackets = totalPackets + Math.floor(object.packetsLost); totalPacketsLost = totalPacketsLost + Math.floor(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); } if (object.packetsLost > 0) { //console.log(identity, mediaType, 'packetsLost', object.packetsLost); } } else if (object.packetsReceived && identity !== 'myself') { totalPackets = totalPackets + Math.floor(object.packetsReceived); if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsReceived); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsReceived); } //console.log(identity, mediaType, 'packetsReceived', object.packetsReceived); } else if (object.packetsSent && identity === 'myself') { totalPackets = totalPackets + Math.floor(object.packetsSent); if (mediaType === 'audio') { audioPackets = audioPackets + Math.floor(object.packetsSent); } else if (mediaType === 'video') { videoPackets = videoPackets + Math.floor(object.packetsSent); } //console.log(identity, mediaType, 'packetsSent', object.packetsSent); } else if (object.googCurrentDelayMs && identity !== 'myself') { delay = object.googCurrentDelayMs; //console.log('mediaType', mediaType, 'identity', identity, 'delay', delay); this.latency.set(p.id, Math.ceil(delay)); //console.log(object); } if (identity === 'myself') { //console.log(object); } }); if (videoPackets > 0) { videoPacketLoss = Math.floor(videoPacketsLost / videoPackets * 100); } else { videoPacketLoss = 100; } if (audioPackets > 0) { audioPacketLoss = Math.floor(audioPacketsLost / audioPackets * 100); } else { audioPacketLoss = 100; } if (totalPackets > 0) { totalPacketLoss = Math.floor(totalPacketsLost / totalPackets * 100); } else { totalPacketLoss = 100; } this.audioPacketLoss.set(p.id, audioPacketLoss); this.videoPacketLoss.set(p.id, videoPacketLoss); this.packetLoss.set(p.id, totalPacketLoss); }}); //console.log(identity, p.id, 'audio loss', audioPacketLoss, '%, video loss', videoPacketLoss, '%, total loss', totalPacketLoss, '%'); const bandwidthDownload = totalVideoBandwidth + totalAudioBandwidth; this.bandwidthDownload = bandwidthDownload; this.bandwidthUpload = bandwidthUpload; this.videoBandwidth.set('total', totalVideoBandwidth); this.audioBandwidth.set('total', totalAudioBandwidth); //console.log('audio bandwidth', totalAudioBandwidth); //console.log('video bandwidth', totalVideoBandwidth); //console.log('total bandwidth', this.bandwidthDownload); //console.log('this.latency', this.latency); }); }); }; onParticipantJoined(p) { console.log(p.identity.uri, 'joined the conference'); if (p.identity._uri.search('guest.') === -1) { if (p.identity._uri !== this.props.call.localIdentity._uri) { // used for history item this.props.saveParticipant(this.props.call.id, this.state.remoteUri, p.identity._uri); } const dn = p.identity._uri + ' joined'; this.postChatSystemMessage(dn, false); } else { this.postChatSystemMessage('An anonymous guest joined', false); } this.lookupContact(p.identity._uri, p.identity._displayName); if (this.invitedParticipants.has(p.identity._uri)) { this.invitedParticipants.delete(p.identity._uri); } // this.refs.audioPlayerParticipantJoined.play(); p.on('stateChanged', this.onParticipantStateChanged); p.attach(); p.timestamp = Date.now(); this.setState({ participants: this.state.participants.concat([p]) }); // this.changeResolution(); this.armOverlayTimer(); } onParticipantLeft(p) { console.log(p.identity.uri, 'left the conference'); const participants = this.state.participants.slice(); this.audioBandwidth.delete(p.id); this.videoBandwidth.delete(p.id); this.latency.delete(p.id); this.audioBytesReceived.delete(p.id); this.videoBytesReceived.delete(p.id); this.audioPacketLoss.delete(p.id); this.videoPacketLoss.delete(p.id); this.packetLoss.delete(p.id); this.mediaLost.delete(p.id); const idx = participants.indexOf(p); if (idx !== -1) { participants.splice(idx, 1); this.setState({ participants: participants }); } p.detach(true); // this.changeResolution(); this.armOverlayTimer(); this.postChatSystemMessage(p.identity.uri + ' left', false); } onParticipantStateChanged(oldState, newState) { if (newState === 'established' || newState === null) { this.maybeSwitchLargeVideo(); } } onConfigureRoom(config) { const newState = {}; newState.activeSpeakers = config.activeParticipants; this.setState(newState); if (config.activeParticipants.length === 0) { this.logEvent.info('set speakers to', ['Nobody'], config.originator); } else { const speakers = config.activeParticipants.map((p) => {return p.identity.displayName || p.identity.uri}); this.logEvent.info('set speakers to', speakers, config.originator); } this.maybeSwitchLargeVideo(); } onFileSharing(files) { let stateFiles = this.state.sharedFiles; stateFiles = stateFiles.concat(files); this.setState({sharedFiles: stateFiles}); this.listSharedFiles(); } onVideoSelected(item) { const participants = this.state.participants.slice(); const idx = participants.indexOf(item); participants.splice(idx, 1); participants.unshift(item); if (item.videoPaused) { item.resumeVideo(); } this.setState({ participants: participants }); } changeResolution() { let stream = this.props.call.getLocalStreams()[0]; if (this.state.participants.length < 2) { this.props.call.scaleLocalTrack(stream, 1.5); } else if (this.state.participants.length < 5) { this.props.call.scaleLocalTrack(stream, 2); } else { this.props.call.scaleLocalTrack(stream, 1); } } selectVideo(item) { DEBUG('Switching video to: %o', item); if (item.stream) { this.setState({selfDisplayedLarge: true, largeVideoStream: item.stream}); } } maybeSwitchLargeVideo() { // Switch the large video to another source, maybe. if (this.state.participants.length === 0 && !this.state.selfDisplayedLarge) { // none of the participants are eligible, show ourselves const item = { stream: this.props.call.getLocalStreams()[0], identity: this.props.call.localIdentity }; this.selectVideo(item); } else if (this.state.selfDisplayedLarge) { this.setState({selfDisplayedLarge: false}); } } handleClipboardButton() { utils.copyToClipboard(this.conferenceUrl); this.props.notificationCenter().postSystemNotification('Join me, maybe?', {body: 'Link copied to the clipboard'}); this.setState({shareOverlayVisible: false}); } handleEmailButton(event) { // if (navigator.userAgent.indexOf('Chrome') > 0) { // let emailWindow = window.open(this.emailLink, '_blank'); // setTimeout(() => { // emailWindow.close(); // }, 500); // } else { // window.open(this.emailLink, '_self'); // } this.setState({shareOverlayVisible: false}); } handleShareOverlayEntered() { this.setState({shareOverlayVisible: true}); } handleShareOverlayExited() { this.setState({shareOverlayVisible: false}); } handleActiveSpeakerSelected(participant, secondVideo=false) { // eslint-disable-line space-infix-ops let newActiveSpeakers = this.state.activeSpeakers.slice(); if (secondVideo) { if (participant.id !== 'none') { if (newActiveSpeakers.length >= 1) { newActiveSpeakers[1] = participant; } else { newActiveSpeakers[0] = participant; } } else { newActiveSpeakers.splice(1,1); } } else { if (participant.id !== 'none') { newActiveSpeakers[0] = participant; } else { newActiveSpeakers.shift(); } } this.toggleDrawer(); this.props.call.configureRoom(newActiveSpeakers.map((element) => element.publisherId), (error) => { if (error) { // This causes a state update, hence the drawer lists update this.logEvent.error('set speakers failed', [], this.localIdentity); } }); } toggleSpeakerSelection() { this.setState({showSpeakerSelection: !this.state.showSpeakerSelection}); } startSpeakerSelection(number) { this.selectSpeaker = number; this.toggleSpeakerSelection(); } preventOverlay(event) { // Stop the overlay when we are the thumbnail bar event.stopPropagation(); } muteAudio(event) { event.preventDefault(); if (this.state.audioMuted) { //this.postChatSystemMessage('Audio un-muted'); this.props.toggleMute(this.props.call.id, false); } else { //this.postChatSystemMessage('Audio muted'); this.props.toggleMute(this.props.call.id, true); } } toggleChat(event) { //event.preventDefault(); if (!this.state.videoEnabled) { if (this.state.chatView && !this.state.audioView) { this.setState({audioView: !this.state.audioView}); } } this.setState({chatView: !this.state.chatView}); } toggleAudioParticipants(event) { //event.preventDefault(); if (this.state.audioView && !this.state.chatView) { this.setState({chatView: !this.state.chatView}); } this.setState({audioView: !this.state.audioView}); } toggleCamera(event) { event.preventDefault(); const localStream = this.props.call.getLocalStreams()[0]; if (localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; track._switchCamera(); } } muteVideo(event) { event.preventDefault(); if (this.state.videoMuted) { this._resumeVideo(); this.setState({videoMutedbyUser: false}); } else { this.setState({videoMutedbyUser: true}); this._muteVideo(); } } _muteVideo() { const localStream = this.props.call.getLocalStreams()[0]; if (localStream && localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; if (!this.state.videoMuted) { console.log('Mute camera'); track.enabled = false; this.setState({videoMuted: true}); } } } _resumeVideo() { const localStream = this.props.call.getLocalStreams()[0]; if (localStream && localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; if (this.state.videoMuted) { console.log('Resume camera'); track.enabled = true; this.setState({videoMuted: false}); } } } hangup(event) { //event.preventDefault(); for (let participant of this.state.participants) { participant.detach(); } this.props.hangup('user_hangup_conference'); } armOverlayTimer() { if (this.props.audioOnly) { return; } this.setState({callOverlayVisible: true}); if (this.state.participants.length > 0) { clearTimeout(this.overlayTimer); this.overlayTimer = setTimeout(() => { this.setState({callOverlayVisible: false}); }, 5000); } } showOverlay() { if (this.props.audioOnly) { return; } // if (!this.state.shareOverlayVisible && !this.state.showDrawer && !this.state.showFiles) { // if (!this.state.callOverlayVisible) { this.setState({callOverlayVisible: !this.state.callOverlayVisible}); // } // this.armOverlayTimer(); // } } toggleInviteModal() { this.setState({showInviteModal: !this.state.showInviteModal}); } toggleDrawer() { this.setState({callOverlayVisible: true, showDrawer: !this.state.showDrawer, showFiles: false, showSpeakerSelection: false}); clearTimeout(this.overlayTimer); } toggleFiles() { this.setState({callOverlayVisible: true, showFiles: !this.state.showFiles, showDrawer: false}); clearTimeout(this.overlayTimer); } showFiles() { this.setState({callOverlayVisible: true, showFiles: true, showDrawer: false}); clearTimeout(this.overlayTimer); } inviteParticipants(uris=[]) { if (uris.length === 0) { return; } //console.log('inviteParticipants', uris); this.props.call.inviteParticipants(uris); uris.forEach((uri) => { uri = uri.replace(/ /g, ''); if (this.props.call.localIdentity._uri === uri) { return; } this.postChatSystemMessage(uri + ' was invited', false); this.invitedParticipants.set(uri, {timestamp: Date.now(), status: 'Invited'}) this.props.saveParticipant(this.props.call.id, this.state.remoteUri, uri); this.lookupContact(uri); }); this.props.finishInvite(); this.forceUpdate() } render() { if (this.props.call === null) { return (); } //console.log('---- Conference box', this.state.renderMessages.length); let watermark; let renderMessages = this.state.renderMessages; //renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); renderMessages = renderMessages.sort(function(a, b) { if (a.createdAt < b.createdAt) { return 1; //nameA comes first } if (a.createdAt > b.createdAt) { return -1; // nameB comes first } if (a.createdAt === b.createdAt) { if (a.msg_id < b.msg_id) { return 1; //nameA comes first } if (a.msg_id > b.msg_id) { return -1; // nameB comes first } } return 0; // names must be equal }); const largeVideoClasses = classNames({ 'animated' : true, 'fadeIn' : true, 'large' : true, 'mirror' : !this.props.call.sharingScreen && !this.props.generatedVideoTrack, 'fit' : this.props.call.sharingScreen }); let matrixClasses = classNames({ 'matrix' : true }); const containerClasses = classNames({ 'video-container': true, 'conference': true, 'drawer-visible': this.state.showDrawer || this.state.showFiles }); const buttons = {}; const muteButtonIcons = this.state.audioMuted ? 'microphone-off' : 'microphone'; const muteVideoButtonIcons = this.state.videoMuted ? 'video-off' : 'video'; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; const conferenceContainer = this.state.isLandscape ? styles.conferenceContainerLandscape : styles.conferenceContainer; let chatContainer = this.state.isLandscape ? styles.chatContainerLandscape : styles.chatContainerPortrait; if (this.props.audioOnly) { chatContainer = this.state.isLandscape ? styles.chatContainerLandscapeAudio : styles.chatContainerPortraitAudio; } // populate speaker selection list only with participants that have video let speakerSelectionParticipants = []; this.state.participants.forEach((p) => { if (p.streams && p.streams.length > 0) { if (p.streams[0].getVideoTracks().length > 0) { let track = p.streams[0].getVideoTracks()[0]; speakerSelectionParticipants.push(p); } } }); //console.log('Number of possible speakers with video enabled', speakerSelectionParticipants.length); let myself = {id: this.props.call.id, publisherId: this.props.call.id, identity: this.props.call.localIdentity}; let unselectItem = {id: 'none', publisherId: null, identity: {uri: 'none', displayName: 'No speaker'}}; speakerSelectionParticipants.push(myself); speakerSelectionParticipants.push(unselectItem); //console.log('----speakerSelectionParticipants', speakerSelectionParticipants); const floatingButtons = []; if (this.state.videoEnabled && this.state.isLandscape) { floatingButtons.push( ); } if (!this.state.chatView && !this.state.showDrawer && speakerSelectionParticipants.length > 2 && this.state.videoEnabled) { floatingButtons.push( ); } if (this.state.videoEnabled) { floatingButtons.push( ); } if (!this.state.videoEnabled ) { floatingButtons.push( ); } if (!this.state.videoEnabled && !this.state.isLandscape) { /* floatingButtons.push( ); */ } if (this.state.videoEnabled && !this.state.chatView) { floatingButtons.push( ); } floatingButtons.push( ); if (this.state.videoEnabled && !this.state.chatView) { floatingButtons.push( ); } if (!this.state.reconnectingCall) { floatingButtons.push( ) } if (this.state.videoEnabled && !this.state.isLandscape) { floatingButtons.push( ); } /* floatingButtons.push( ); */ /* floatingButtons.push( ); */ if (this.props.isLandscape && !this.state.chatView && !this.props.audioOnly) { buttons.additional = floatingButtons; } else { buttons.additional = []; } /* buttons.additional.push( ); */ /* floatingButtons.push( ); */ const audioParticipants = []; let _contact; let _identity; let participants_uris = []; let sessionButtons = floatingButtons; let inviteParticipantsModal = ( {return p.identity.uri})} close={this.toggleInviteModal} room={this.state.remoteUri} defaultDomain = {this.props.defaultDomain} accountId = {this.props.call.localIdentity._uri} notificationCenter = {this.props.notificationCenter} lookupContacts = {this.props.lookupContacts} /> ); if (this.props.audioOnly) { sessionButtons = []; buttons.additional = []; this.state.participants.forEach((p) => { _contact = this.foundContacts.get(p.identity._uri); _identity = {uri: p.identity._uri.indexOf('@guest') > -1 ? 'From the web': p.identity._uri, displayName: (_contact && _contact.displayName != p.identity._displayName) ? _contact.displayName : p.identity._displayName, photo: _contact ? _contact.photo: null }; participants_uris.push(p.identity._uri); let status = ''; let duration = 0; if (p.timestamp) { duration = Math.floor(new Date() - p.timestamp) / 1000; if (duration > 3600) { status = moment.duration(new Date() - p.timestamp).format('hh:mm:ss', {trim: false}); } else { status = moment.duration(new Date() - p.timestamp).format('mm:ss', {trim: false}); } } audioParticipants.push( 10 ? this.packetLoss.get(p.id) : 0} timestamp={p.timestamp} isLocal={false} status={status} supportsVideo={this.state.call ? this.state.call.supportsVideo: false} /> ); }); const invitedParties = Array.from(this.invitedParticipants.keys()); let alreadyInvitedParticipants = [] let p; invitedParties.forEach((_uri) => { if (participants_uris.indexOf(_uri) > 0) { return; } p = this.invitedParticipants.get(_uri); _contact = this.foundContacts.get(_uri); _identity = {uri: _uri, displayName: (_contact && _contact.displayName ) ? _contact.displayName : _uri, photo: _contact ? _contact.photo: null }; if (p.status != 'No answer') { alreadyInvitedParticipants.push(_uri) } //console.log('p.status', p.status); let extraButtons = []; let invite_uris = []; invite_uris.push(_uri); if (p.status === 'reinvite') { extraButtons.push( this.removeInvitedParticipant(_uri)} /> ); extraButtons.push( this.inviteParticipants(invite_uris)} /> ); } audioParticipants.push( ); }); const audioContainer = this.state.isLandscape ? styles.audioContainerLandscape : styles.audioContainerPortrait; audioParticipants.sort((a, b) => (a.timestamp < b.timestamp) ? 1 : -1) _contact = this.foundContacts.get(this.props.call.localIdentity._uri); _identity = {uri: this.props.call.localIdentity._uri, displayName: _contact.displayName, photo: _contact.photo }; participants_uris.push(this.props.call.localIdentity._uri); audioParticipants.splice(0, 0, ); if (this.state.isLandscape) { return ( {inviteParticipantsModal} {sessionButtons} {audioParticipants} { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} alwaysShowSend={true} scrollToBottom lockStyle={styles.lock} inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} + timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} infiniteScroll /> ); } else { return ( {inviteParticipantsModal} {!this.state.keyboardVisible && !this.props.isLandscape ? {sessionButtons} : null} {!this.state.keyboardVisible ? {audioParticipants} : null} {/* Divider */} {/* Bottom View */} {!this.state.isDividerClicked ? { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} alwaysShowSend={true} lockStyle={styles.lock} scrollToBottom inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} + timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} infiniteScroll /> : null} ); } } const participants = []; const drawerParticipants = []; if (this.state.participants.length > 0) { if (this.state.activeSpeakers.findIndex((element) => {return element.id === this.props.call.id}) === -1) { participants.push( ); } } drawerParticipants.push( ); let videos = []; let status = ''; if (this.state.participants.length === 0) { videos.push( ); } else { const activeSpeakers = this.state.activeSpeakers; const activeSpeakersCount = activeSpeakers.length; if (activeSpeakersCount > 0) { activeSpeakers.forEach((p) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms'; } if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } videos.push( ); }); this.state.participants.forEach((p) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms delay'; } if (this.state.activeSpeakers.indexOf(p) === -1) { participants.push( {}} pauseVideo={true} display={false} status={status} /> ); } drawerParticipants.push( ); }); } else { let vtrack; this.state.participants.forEach((p, idx) => { status = ''; if (this.mediaLost.has(p.id) && this.mediaLost.get(p.id)) { status = 'Muted'; } else if (this.packetLoss.has(p.id) && this.packetLoss.get(p.id) > 3) { if (this.packetLoss.get(p.id) === 100) { status = 'No media'; return; } else { status = this.packetLoss.get(p.id) + '% loss'; } } else if (this.latency.has(p.id) && this.latency.get(p.id) > 100) { status = this.latency.get(p.id) + ' ms'; } if (p.streams && p.streams.length > 0) { if (p.streams[0].getVideoTracks().length > 0) { vtrack = p.streams[0].getVideoTracks()[0]; if (vtrack.muted) { //console.log('Skip muted video of', p.identity.uri); return; } } } // console.log('Added video of', p.identity.uri); videos.push( = 4) || (idx >= 2 && this.props.isTablet === false)} isLandscape={this.state.isLandscape} isTablet={this.props.isTablet} useTwoRows={this.state.participants.length > 2} status={status} /> ); if (idx >= 4 || idx >= 2 && this.props.isTablet === false) { participants.push( ); } drawerParticipants.push( ); }); } } const currentParticipants = this.state.participants.map((p) => {return p.identity.uri}) const alreadyInvitedParticipants = this.invitedParticipants ? Array.from(this.invitedParticipants.keys()) : []; if (this.state.callOverlayVisible) { buttons.bottom = floatingButtons; buttons.additional = []; } return ( {inviteParticipantsModal} {this.state.callOverlayVisible || this.state.chatView ? : null} {videos} {this.state.chatView ? { return (!_.isEqual(props.currentMessage, nextProps.currentMessage)); }} - alwaysShowSend={true} scrollToBottom inverted={true} - timeTextStyle={{ left: { color: 'red' }, right: { color: 'yellow' } }} + timeTextStyle={{ left: { color: 'red' }, right: { color: 'black' } }} infiniteScroll /> : {participants} } {drawerParticipants} ); } } ConferenceBox.propTypes = { notificationCenter : PropTypes.func.isRequired, call : PropTypes.object, connection : PropTypes.object, hangup : PropTypes.func, saveParticipant : PropTypes.func, saveConferenceMessage: PropTypes.func, updateConferenceMessage : PropTypes.func, deleteConferenceMessage : PropTypes.func, messages : PropTypes.array, previousParticipants: PropTypes.array, remoteUri : PropTypes.string, generatedVideoTrack : PropTypes.bool, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, isLandscape : PropTypes.bool, isTablet : PropTypes.bool, muted : PropTypes.bool, defaultDomain : PropTypes.string, inFocus : PropTypes.bool, reconnectingCall : PropTypes.bool, audioOnly : PropTypes.bool, initialParticipants : PropTypes.array, terminated : PropTypes.bool, myContacts : PropTypes.object, lookupContacts : PropTypes.func, goBackFunc : PropTypes.func, inviteToConferenceFunc: PropTypes.func, selectedContacts : PropTypes.array, callState : PropTypes.object, callContact : PropTypes.object, finishInvite : PropTypes.func, account : PropTypes.object, messages : PropTypes.object, getMessages : PropTypes.func, fileSharingUrl : PropTypes.string, sendConferenceMessage: PropTypes.func, conferenceSliderPosition: PropTypes.number, saveSliderFunc: PropTypes.func }; export default ConferenceBox; diff --git a/app/components/ConferenceModal.js b/app/components/ConferenceModal.js index 774aa57..feb13b1 100644 --- a/app/components/ConferenceModal.js +++ b/app/components/ConferenceModal.js @@ -1,306 +1,304 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { View, TouchableOpacity, FlatList } from 'react-native'; import Autocomplete from 'react-native-autocomplete-input'; import { Portal, Dialog, Button, Text, TextInput, Surface, Chip} from 'react-native-paper'; import KeyboardAwareDialog from './KeyBoardAwareDialog'; const DialogType = Platform.OS === 'ios' ? KeyboardAwareDialog : Dialog; import config from '../config'; import styles from '../assets/styles/blink/_ConferenceModal.scss'; class ConferenceModal extends Component { constructor(props) { super(props); let targetUri = props.targetUri ? props.targetUri.split('@')[0] : ''; this.state = { targetUri: targetUri, myInvitedParties: props.myInvitedParties, selectedContact: props.selectedContact, participants: [], filteredContacts: [], searching: false, roomUrl: config.publicUrl + '/conference/' + targetUri }; this.handleConferenceTargetChange = this.handleConferenceTargetChange.bind(this); this.onHide = this.onHide.bind(this); this.joinAudio = this.joinAudio.bind(this); this.joinVideo = this.joinVideo.bind(this); } componentDidMount() { this.handleConferenceTargetChange(this.state.targetUri); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { let uri = nextProps.targetUri.split('@')[0]; this.setState({myInvitedParties: nextProps.myInvitedParties, selectedContact: nextProps.selectedContact, participants: nextProps.participants || [] }); - if (nextProps.targetUri) { - this.handleConferenceTargetChange(uri); - } + this.handleConferenceTargetChange(uri); } handleConferenceTargetChange(value) { let targetUri = value; let participants = []; let sanitizedParticipants = []; let username; let domain; if (targetUri) { let uri = `${targetUri.replace(/[\s\@()]/g, '')}@${config.defaultConferenceDomain}`; uri = uri.split('@')[0]; if (this.state.selectedContact && this.state.selectedContact.participants) { participants = this.state.selectedContact.participants; } else if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(uri)) { participants = this.state.myInvitedParties[uri]; } participants.forEach((item) => { item = item.trim().toLowerCase(); if (item.length === 0) { return; } const username = item.split('@')[0]; if (username.length === 0) { return; } const domain = item.split('@')[1]; if (item === this.props.accountId) { return; } if (item.indexOf('@') === -1) { sanitizedParticipants.push(item); } else { if (domain === this.props.defaultDomain) { sanitizedParticipants.push(username); } else { sanitizedParticipants.push(item); } } }); } this.setState({targetUri: targetUri, searching: false, roomUrl: targetUri ? config.publicUrl + '/conference/' + targetUri: '', participants: sanitizedParticipants }); } joinAudio(event) { event.preventDefault(); if (!this.state.targetUri) { return; } const uri = `${this.state.targetUri.replace(/[\s\@()]/g, '')}@${config.defaultConferenceDomain}`; const participants = []; if (this.state.participants) { this.state.participants.forEach((item) => { item = item.trim().toLowerCase().replace(' ', ''); if (item.length > 1) { if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } participants.push(item); } }); } this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: false, participants: participants}); } joinVideo(event) { event.preventDefault(); const uri = `${this.state.targetUri.replace(/[\s\@()]/g, '')}@${config.defaultConferenceDomain}`; const participants = []; if (this.state.participants) { this.state.participants.forEach((item) => { item = item.trim().toLowerCase().replace(' ', ''); if (item.indexOf('@') === -1) { item = `${item}@${this.props.defaultDomain}`; } participants.push(item); }); } this.props.handleConferenceCall(uri.toLowerCase(), {audio: true, video: true, participants: participants}); } onHide() { this.props.handleConferenceCall(null); } isValidUri(uri) { if (uri === this.props.accountId) { return false; } let username = uri.split('@')[0]; let domain = uri.split('@')[1]; if (username.match(/^(\+?)([\-|\d]+)$/) && !domain) { return false; } if (domain) { if (domain.indexOf('yahoo') > -1) { return false; } if (domain.indexOf('icloud') > -1) { return false; } if (domain.indexOf('gmail') > -1) { return false; } } return true; } removeParticipant(uri) { let participants = this.state.participants; let idx = participants.indexOf(uri); if (idx > -1) { //console.log('Remove', uri, 'from', participants); participants.splice(idx, 1); this.setState({participants: participants}); } } updateParticipants(contact) { let participants = this.state.participants; let els = participants; if (els.length === 1) { participants = contact.uri; } else { els.pop(-1); els.push(contact.uri); participants = els.toString(','); } this.setState({participants: participants.replace(/,/g, ", "), filteredContacts: [], searching: false}); } render() { const validUri = this.state.targetUri.length > 0 && this.state.targetUri.indexOf('@') === -1; let data = []; if (this.state.participants) { this.state.participants.forEach((p) => { data.push({key: p.trim()}); }); } return ( Join conference {!this.state.selectedContact ? {this.handleConferenceTargetChange(text);}} name="uri" required defaultValue={this.state.targetUri} /> : {this.state.targetUri} } {this.state.participants.length > 0 ? Invited participants: this.removeParticipant(item.key)}> {item.key} } /> : null} Others can be invited once the conference starts ); } } ConferenceModal.propTypes = { show: PropTypes.bool.isRequired, handleConferenceCall: PropTypes.func.isRequired, myInvitedParties: PropTypes.object, accountId: PropTypes.string, selectedContact: PropTypes.object, targetUri: PropTypes.string.isRequired, defaultDomain: PropTypes.string, lookupContacts: PropTypes.func }; export default ConferenceModal; diff --git a/app/components/ContactsListBox.js b/app/components/ContactsListBox.js index b5c7a82..1e0fa26 100644 --- a/app/components/ContactsListBox.js +++ b/app/components/ContactsListBox.js @@ -1,1349 +1,2151 @@ import React, { Component} from 'react'; import autoBind from 'auto-bind'; - import PropTypes from 'prop-types'; -import { Clipboard, SafeAreaView, View, FlatList, Text, Linking, PermissionsAndroid, Switch} from 'react-native'; - +import { Image, Clipboard, Dimensions, SafeAreaView, View, FlatList, Text, Linking, PermissionsAndroid, Switch, TouchableOpacity, BackHandler, TouchableHighlight} from 'react-native'; import ContactCard from './ContactCard'; import utils from '../utils'; import DigestAuthRequest from 'digest-auth-request'; import uuid from 'react-native-uuid'; -import { GiftedChat, IMessage, Bubble, MessageText } from 'react-native-gifted-chat' +import { GiftedChat, IMessage, Bubble, MessageText, Send, InputToolbar, MessageImage, Time} from 'react-native-gifted-chat' +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' import MessageInfoModal from './MessageInfoModal'; import ShareMessageModal from './ShareMessageModal'; +import DeleteMessageModal from './DeleteMessageModal'; import CustomChatActions from './ChatActions'; import FileViewer from 'react-native-file-viewer'; +import OpenPGP from "react-native-fast-openpgp"; +import DocumentPicker from 'react-native-document-picker'; +import AudioRecorderPlayer from 'react-native-audio-recorder-player'; +import VideoPlayer from 'react-native-video-player'; +import RNFetchBlob from "rn-fetch-blob"; +import { IconButton} from 'react-native-paper'; +import ImageViewer from 'react-native-image-zoom-viewer'; + +import Sound from 'react-native-sound'; +import SoundPlayer from 'react-native-sound-player'; import moment from 'moment'; import momenttz from 'moment-timezone'; -//import Video from 'react-native-video'; +import Video from 'react-native-video'; const RNFS = require('react-native-fs'); import CameraRoll from "@react-native-community/cameraroll"; +import {launchCamera, launchImageLibrary} from 'react-native-image-picker'; +import ImageResizer from 'react-native-image-resizer'; import styles from '../assets/styles/blink/_ContactsListBox.scss'; - String.prototype.toDate = function(format) { var normalized = this.replace(/[^a-zA-Z0-9]/g, '-'); var normalizedFormat= format.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-'); var formatItems = normalizedFormat.split('-'); var dateItems = normalized.split('-'); var monthIndex = formatItems.indexOf("mm"); var dayIndex = formatItems.indexOf("dd"); var yearIndex = formatItems.indexOf("yyyy"); var hourIndex = formatItems.indexOf("hh"); var minutesIndex = formatItems.indexOf("ii"); var secondsIndex = formatItems.indexOf("ss"); var today = new Date(); var year = yearIndex>-1 ? dateItems[yearIndex] : today.getFullYear(); var month = monthIndex>-1 ? dateItems[monthIndex]-1 : today.getMonth()-1; var day = dayIndex>-1 ? dateItems[dayIndex] : today.getDate(); var hour = hourIndex>-1 ? dateItems[hourIndex] : today.getHours(); var minute = minutesIndex>-1 ? dateItems[minutesIndex] : today.getMinutes(); var second = secondsIndex>-1 ? dateItems[secondsIndex] : today.getSeconds(); return new Date(year,month,day,hour,minute,second); }; +const audioRecorderPlayer = new AudioRecorderPlayer(); + +// Note: copy and paste all styles in App.js from my repository +function renderBubble (props) { + let leftColor = 'green'; + let rightColor = '#fff'; + + if (props.currentMessage.failed) { + rightColor = 'red'; + leftColor = 'red'; + } else { + if (props.currentMessage.pinned) { + rightColor = '#2ecc71'; + leftColor = '#2ecc71'; + } + } + + if (props.currentMessage.image) { + return ( + + ) + } else if (props.currentMessage.video) { + return ( + + ) + } else if (props.currentMessage.audio) { + return ( + + ) + } else { + return ( + + ) + } + } + + class ContactsListBox extends Component { constructor(props) { super(props); autoBind(this); this.chatListRef = React.createRef(); + this.default_placeholder = 'Enter message...' let renderMessages = []; if (this.props.selectedContact) { let uri = this.props.selectedContact.uri; if (uri in this.props.messages) { renderMessages = this.props.messages[uri]; //renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); renderMessages = renderMessages.sort(function(a, b) { if (a.createdAt < b.createdAt) { return 1; //nameA comes first } if (a.createdAt > b.createdAt) { return -1; // nameB comes first } if (a.createdAt === b.createdAt) { if (a.msg_id < b.msg_id) { return 1; //nameA comes first } if (a.msg_id > b.msg_id) { return -1; // nameB comes first } } return 0; // names must be equal }); } } this.state = { accountId: this.props.account ? this.props.account.id : null, password: this.props.password, targetUri: this.props.selectedContact ? this.props.selectedContact.uri : this.props.targetUri, favoriteUris: this.props.favoriteUris, blockedUris: this.props.blockedUris, isRefreshing: false, isLandscape: this.props.isLandscape, contacts: this.props.contacts, myInvitedParties: this.props.myInvitedParties, refreshHistory: this.props.refreshHistory, selectedContact: this.props.selectedContact, myContacts: this.props.myContacts, messages: this.props.messages, renderMessages: GiftedChat.append(renderMessages, []), chat: this.props.chat, pinned: false, showMessageModal: false, message: null, showShareMessageModal: false, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectedContacts: this.props.selectedContacts, pinned: this.props.pinned, filter: this.props.filter, periodFilter: this.props.periodFilter, scrollToBottom: true, messageZoomFactor: this.props.messageZoomFactor, isTyping: false, isLoadingEarlier: false, fontScale: this.props.fontScale, call: this.props.call, isTablet: this.props.isTablet, ssiCredentials: this.props.ssiCredentials, - ssiConnections: this.props.ssiConnections + ssiConnections: this.props.ssiConnections, + keys: this.props.keys, + recording: false, + playing: false, + texting: false, + audioRecording: null, + cameraAsset: null, + placeholder: this.default_placeholder, + audioSendFinished: false, + messagesCategoryFilter: this.props.messagesCategoryFilter, + isTexting: this.props.isTexting, + showDeleteMessageModal: false } this.ended = false; + this.recordingTimer = null; + this.outgoingPendMessages = {}; + BackHandler.addEventListener('hardwareBackPress', this.backPressed); + this.listenforSoundNotifications() } componentDidMount() { this.ended = false; } componentWillUnmount() { this.ended = true; + this.stopRecordingTimer() + } + + backPressed() { + this.stopRecordingTimer() } //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.messagesCategoryFilter !== this.state.messagesCategoryFilter && nextProps.selectedContact) { + this.props.getMessages(nextProps.selectedContact.uri, {category: nextProps.messagesCategoryFilter, pinned: this.state.pinned}); + } + + if (nextProps.pinned !== this.state.pinned && nextProps.selectedContact) { + this.props.getMessages(nextProps.selectedContact.uri, {category: nextProps.messagesCategoryFilter, pinned: nextProps.pinned}); + } + if (nextProps.selectedContact !== this.state.selectedContact) { //console.log('Selected contact changed to', nextProps.selectedContact); - + this.resetContact() this.setState({selectedContact: nextProps.selectedContact}); if (nextProps.selectedContact) { this.setState({scrollToBottom: true}); - if (Object.keys(this.state.messages).indexOf(nextProps.selectedContact.uri) === -1) { + if (Object.keys(this.state.messages).indexOf(nextProps.selectedContact.uri) === -1 && nextProps.selectedContact) { this.props.getMessages(nextProps.selectedContact.uri); } } else { this.setState({renderMessages: []}); } }; if (nextProps.myContacts !== this.state.myContacts) { this.setState({myContacts: nextProps.myContacts}); }; if (nextProps.selectedContact) { let renderMessages = []; let uri = nextProps.selectedContact.uri; if (uri in nextProps.messages) { renderMessages = nextProps.messages[uri]; - if (this.state.renderMessages.length !== renderMessages.length) { + if (this.state.renderMessages.length < renderMessages.length) { + //console.log('Number of messages changed', this.state.renderMessages.length, '->', renderMessages.length); this.setState({isLoadingEarlier: false}); this.props.confirmRead(uri); if (this.state.renderMessages.length > 0 && renderMessages.length > 0) { let last_message_ts = this.state.renderMessages[0].createdAt; if (renderMessages[0].createdAt > last_message_ts) { this.setState({scrollToBottom: true}); } } } } - if (renderMessages !== this.state.renderMessages) { - //renderMessages.sort((a, b) => (a.createdAt < b.createdAt) ? 1 : -1); - renderMessages = renderMessages.sort(function(a, b) { + let delete_ids = []; + Object.keys(this.outgoingPendMessages).forEach((_id) => { + if (renderMessages.some((obj) => obj._id === _id)) { + console.log('Remove pending message id', _id); + delete_ids.push(_id); + // message exists + } else { + if (this.state.renderMessages.some((obj) => obj._id === _id)) { + console.log('Pending message id', _id, 'already exists'); + } else { + console.log('Adding pending message id', _id); + renderMessages.push(this.outgoingPendMessages[_id]); + } + } + }); + + delete_ids.forEach((_id) => { + delete this.outgoingPendMessages[_id]; + }); + + renderMessages = renderMessages.sort(function(a, b) { if (a.createdAt < b.createdAt) { return 1; //nameA comes first } if (a.createdAt > b.createdAt) { return -1; // nameB comes first } if (a.createdAt === b.createdAt) { if (a.msg_id < b.msg_id) { return 1; //nameA comes first } if (a.msg_id > b.msg_id) { return -1; // nameB comes first } } - return 0; // names must be equal - }); + }); - this.setState({renderMessages: GiftedChat.append(renderMessages, [])}); - if (!this.state.scrollToBottom && renderMessages.length > 0) { - //console.log('Scroll to first message'); - //this.scrollToMessage(0); - } + this.setState({renderMessages: GiftedChat.append(renderMessages, [])}); + if (!this.state.scrollToBottom && renderMessages.length > 0) { + //console.log('Scroll to first message'); + //this.scrollToMessage(0); } } this.setState({isLandscape: nextProps.isLandscape, isTablet: nextProps.isTablet, chat: nextProps.chat, fontScale: nextProps.fontScale, filter: nextProps.filter, call: nextProps.call, password: nextProps.password, showMessageModal: nextProps.showMessageModal, messages: nextProps.messages, inviteContacts: nextProps.inviteContacts, shareToContacts: nextProps.shareToContacts, selectedContacts: nextProps.selectedContacts, pinned: nextProps.pinned, isTyping: nextProps.isTyping, periodFilter: nextProps.periodFilter, ssiCredentials: nextProps.ssiCredentials, ssiConnections: nextProps.ssiConnections, - targetUri: nextProps.selectedContact ? nextProps.selectedContact.uri : nextProps.targetUri + messagesCategoryFilter: nextProps.messagesCategoryFilter, + targetUri: nextProps.selectedContact ? nextProps.selectedContact.uri : nextProps.targetUri, + keys: nextProps.keys, + isTexting: nextProps.isTexting, + showDeleteMessageModal: nextProps.showDeleteMessageModal }); if (nextProps.isTyping) { setTimeout(() => { this.setState({isTyping: false}); }, 3000); } } + listenforSoundNotifications() { + // Subscribe to event(s) you want when component mounted + this._onFinishedPlayingSubscription = SoundPlayer.addEventListener('FinishedPlaying', ({ success }) => { + //console.log('finished playing', success) + this.setState({playing: false, placeholder: this.default_placeholder}); + }) + 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) + }) + } + + async _launchCamera() { + let options = {maxWidth: 1000, + cameraType: 'front' + } + await launchCamera(options, this.cameraCallback); + } + + async _launchImageLibrary() { + let options = {maxWidth: 1000 + } + await launchImageLibrary(options, this.cameraCallback); + } + + async cameraCallback(result) { + if (result.assets.length === 0) { + return; + } + + let asset = result.assets[0]; + asset.preview = true; + let msg = await this.file2GiftedChat(asset); + console.log(msg); + this.outgoingPendMessages[msg.metadata.transfer_id] = msg; + this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg]), + cameraAsset: msg, + placeholder: 'Delete or send image...' + }); + } + + renderMessageImage =(props) => { + /* + return( + this.onMessagePress(context, props.currentMessage)}> + + + ); +*/ + return ( + + ) + } + renderCustomActions = props => ( - + ) - onSendFromUser() { - console.log('On send from user...'); + customInputToolbar = props => { + return ( + {this.renderComposer}} + containerStyle={styles.chatInsideRightActionsContainer} + /> + ); + }; + + chatInputChanged(text) { + this.setState({texting: (text.length > 0)}) + } + + resetContact() { + this.stopRecordingTimer() + this.setState({ + recording: false, + texting: false, + audioRecording: null, + cameraAsset: null, + placeholder: this.default_placeholder, + audioSendFinished: false + }); + } + + renderComposer(props) { + return( + this.setState({ composerText: text })} + text={this.state.composerText} + multiline={true} + placeholderTextColor={'red'} + > + ) + } + + onRecording(state) { + this.setState({recording: state}); + if (state) { + this.startRecordingTimer(); + } else { + this.stopRecordingTimer() + } + } + + startRecordingTimer() { + let i = 0; + this.setState({placeholder: 'Recording audio'}); + this.recordingTimer = setInterval(() => { + i = i + 1 + this.setState({placeholder: 'Recording audio ' + i + 's'}); + }, 1000); } + stopRecordingTimer() { + if (this.recordingTimer) { + clearInterval(this.recordingTimer); + this.recordingTimer = null; + this.setState({placeholder: this.default_placeholder}); + } + } + + updateMessageMetadata(metadata) { + let renderMessages = this.state.renderMessages; + let newRenderMessages = []; + renderMessages.forEach((message) => { + if (metadata.transfer_id === message._id) { + message.metadata = metadata; + } + newRenderMessages.push(message); + }); + + this.setState({renderMessages: GiftedChat.append(newRenderMessages, [])}); + } + + async startPlaying(message) { + if (this.state.playing || this.state.recording) { + console.log('Already playing or recording'); + return; + } + + this.setState({playing: true, placeholder: 'Playing audio message'}); + message.metadata.playing = true; + this.updateMessageMetadata(message.metadata); + + if (Platform.OS === "android") { + const msg = await audioRecorderPlayer.startPlayer(message.audio); + console.log('Audio playback started', message.audio); + audioRecorderPlayer.addPlayBackListener((e) => { + //console.log('duration', e.duration, e.currentPosition); + if (e.duration === e.currentPosition) { + this.setState({playing: false, placeholder: this.default_placeholder}); + //console.log('Audio playback ended', message.audio); + message.metadata.playing = false; + this.updateMessageMetadata(message.metadata); + } + this.setState({ + currentPositionSec: e.currentPosition, + currentDurationSec: e.duration, + playTime: audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)), + duration: audioRecorderPlayer.mmssss(Math.floor(e.duration)), + }); + }); + } else { + + /* + console.log('startPlaying', file); + + this.sound = new Sound(file, '', error => { + if (error) { + console.log('failed to load the file', file, error); + } + }); + return; + */ + try { + SoundPlayer.playUrl('file://'+message.audio); + this.setState({playing: true, placeholder: 'Playing audio message'}); + } catch (e) { + console.log(`cannot play the sound file`, e) + } + + try { + const info = await SoundPlayer.getInfo() // Also, you need to await this because it is async + console.log('Sound info', info) // {duration: 12.416, currentTime: 7.691} + } catch (e) { + console.log('There is no song playing', e) + } + } + }; + + async stopPlaying(message) { + console.log('Audio playback ended', message.audio); + this.setState({playing: false, placeholder: this.default_placeholder}); + message.metadata.playing = false; + this.updateMessageMetadata(message.metadata); + if (Platform.OS === "android") { + const msg = await audioRecorderPlayer.stopPlayer(); + } else { + SoundPlayer.stop(); + } + } + + async audioRecorded(file) { + const placeholder = file ? 'Delete or send audio...' : this.default_placeholder; + if (file) { + console.log('Audio recording ready to send', file); + } else { + console.log('Audio recording removed'); + } + this.setState({recording: false, placeholder: placeholder, audioRecording: file}); + } + + renderSend = (props) => { + if (this.state.recording) { + return ( + + + ); + } else { + if (this.state.cameraAsset) { + return ( + + + + + + + + + + + ); + + } else if (this.state.audioRecording) { + return ( + + + + + + + + ); + } else { + + if (this.state.playing) { + return ; + } else { + return ( + + + {this.state.texting ? + null + : + + + + } + {this.state.texting ? + null + : + + + + } + + + + ); + } + } + } + }; + setTargetUri(uri, contact) { //console.log('Set target uri uri in history list', uri); this.props.setTargetUri(uri, contact); } setFavoriteUri(uri) { return this.props.setFavoriteUri(uri); } setBlockedUri(uri) { return this.props.setBlockedUri(uri); } renderItem(object) { let item = object.item || object; let invitedParties = []; let uri = item.uri; let myDisplayName; let username = uri.split('@')[0]; if (this.state.myContacts && this.state.myContacts.hasOwnProperty(uri)) { myDisplayName = this.state.myContacts[uri].name; } if (this.state.myInvitedParties && this.state.myInvitedParties.hasOwnProperty(username)) { invitedParties = this.state.myInvitedParties[username]; } if (myDisplayName) { if (item.name === item.uri || item.name !== myDisplayName) { item.name = myDisplayName; } } return( ); } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } closeMessageModal() { this.setState({showMessageModal: false, message: null}); } loadEarlierMessages() { //console.log('Load earlier messages...'); this.setState({scrollToBottom: false, isLoadingEarlier: true}); this.props.loadEarlierMessages(); } - onSendWithFile(selectedFile) { - let uri; - if (!this.state.selectedContact) { - if (this.state.targetUri && this.state.chat) { - let contacts = this.searchedContact(this.state.targetUri); - if (contacts.length !== 1) { - return; - } - uri = contacts[0].uri; - } else { - return; - } - } else { - uri = this.state.selectedContact.uri; - } - - let fileData = { - name: selectedFile.name, - type: selectedFile.type, - size: selectedFile.size, - uri: selectedFile.uri - }; - - console.log('Sending file', fileData); - //this.props.sendMessage(uri, message); - } - onSendMessage(messages) { let uri; if (!this.state.selectedContact) { if (this.state.targetUri && this.state.chat) { let contacts = this.searchedContact(this.state.targetUri); if (contacts.length !== 1) { return; } uri = contacts[0].uri; } else { return; } } else { uri = this.state.selectedContact.uri; } messages.forEach((message) => { /* sent: true, // Mark the message as received, using two tick received: true, // Mark the message as pending with a clock loader pending: true, */ message.encrypted = this.state.selectedContact && this.state.selectedContact.publicKey ? 2 : 0; this.props.sendMessage(uri, message); }); this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, messages)}); } searchedContact(uri, contact=null) { if (uri.indexOf(' ') > -1) { return []; } const item = this.props.newContactFunc(uri.toLowerCase(), null, {src: 'search_contact'}); if (!item) { return []; } if (contact) { item.name = contact.name; item.photo = contact.photo; } return [item]; } - getServerHistory() { if (!this.state.accountId) { return; } if (this.ended || !this.state.accountId || this.state.isRefreshing) { return; } //console.log('Get server history...'); this.setState({isRefreshing: true}); let history = []; let localTime; let getServerCallHistory = new DigestAuthRequest( 'GET', `${this.props.config.serverCallHistoryUrl}?action=get_history&realm=${this.state.accountId.split('@')[1]}`, this.state.accountId.split('@')[0], this.state.password ); // Disable logging getServerCallHistory.loggingOn = false; getServerCallHistory.request((data) => { if (data.success !== undefined && data.success === false) { console.log('Error getting call history from server', data.error_message); return; } if (data.received) { data.received.map(elem => {elem.direction = 'incoming'; return elem}); history = history.concat(data.received); } if (data.placed) { data.placed.map(elem => {elem.direction = 'outgoing'; return elem}); history = history.concat(data.placed); } history.sort((a, b) => (a.startTime < b.startTime) ? 1 : -1) if (history) { const known = []; history = history.filter((elem) => { elem.conference = false; elem.id = uuid.v4(); if (!elem.tags) { elem.tags = []; } if (elem.remoteParty.indexOf('@conference.') > -1) { return null; } elem.uri = elem.remoteParty.toLowerCase(); let uri_els = elem.uri.split('@'); let username = uri_els[0]; let domain; if (uri_els.length > 1) { domain = uri_els[1]; } if (elem.uri.indexOf('@guest.') > -1) { if (!elem.displayName) { elem.uri = 'guest@' + elem.uri.split('@')[1]; } else { elem.uri = elem.displayName.toLowerCase().replace(/\s|\-|\(|\)/g, '') + '@' + elem.uri.split('@')[1]; } } if (utils.isPhoneNumber(elem.uri)) { username = username.replace(/\s|\-|\(|\)/g, ''); username = username.replace(/^00/, "+"); elem.uri = username; } if (known.indexOf(elem.uri) > -1) { return null; } known.push(elem.uri); if (elem.displayName) { elem.name = elem.displayName; } else { elem.name = elem.uri; } if (elem.remoteParty.indexOf('@videoconference.') > -1) { elem.conference = true; elem.media = ['audio', 'video', 'chat']; } if (elem.uri === this.state.accountId) { elem.name = this.props.myDisplayName || 'Myself'; } if (!elem.media || !Array.isArray(elem.media)) { elem.media = ['audio']; } if (elem.timezone !== undefined) { localTime = momenttz.tz(elem.startTime, elem.timezone).toDate(); elem.startTime = localTime; elem.timestamp = localTime; localTime = momenttz.tz(elem.stopTime, elem.timezone).toDate(); elem.stopTime = localTime; } if (elem.direction === 'incoming' && elem.duration === 0) { elem.tags.push('missed'); } return elem; }); this.props.saveHistory(history); if (this.ended) { return; } this.setState({isRefreshing: false}); } }, (errorCode) => { console.log('Error getting call history from server', errorCode); }); this.setState({isRefreshing: false}); } + deleteCameraAsset() { + console.log('deleteCameraAsset'); + if (this.state.cameraAsset && this.state.cameraAsset.metadata.transfer_id in this.outgoingPendMessages) { + console.log('deleted pending message'); + delete this.outgoingPendMessages[this.state.cameraAsset.metadata.transfer_id] + } + this.setState({cameraAsset: null, placeholder: this.default_placeholder}); + this.props.getMessages(this.state.selectedContact.uri); + } + + sendCameraAsset() { + this.transferFile(this.state.cameraAsset); + this.setState({cameraAsset: null, placeholder: this.default_placeholder}); + } + + async sendAudioFile() { + if (this.state.audioRecording) { + this.setState({audioSendFinished: true, placeholder: this.default_placeholder}); + setTimeout(() => { + this.setState({audioSendFinished: false}); + }, 10); + let msg = await this.file2GiftedChat(this.state.audioRecording); + this.transferFile(msg); + this.setState({audioRecording: null}); + } + } + + async _pickDocument() { + try { + const result = await DocumentPicker.pick({ + type: [DocumentPicker.types.allFiles], + copyTo: 'documentDirectory', + mode: 'import', + allowMultiSelection: false, + }); + + const fileUri = result[0].fileCopyUri; + if (!fileUri) { + console.log('File URI is undefined or null'); + return; + } + + let msg = await this.file2GiftedChat(fileUri); + this.transferFile(msg); + + } catch (err) { + if (DocumentPicker.isCancel(err)) { + console.log('User cancelled file picker'); + } else { + console.log('DocumentPicker err => ', err); + throw err; + } + } + }; + + postChatSystemMessage(text, imagePath=null) { + var id = uuid.v4(); + let giftedChatMessage; + + if (imagePath) { + giftedChatMessage = { + _id: id, + key: id, + createdAt: new Date(), + text: text, + image: 'file://' + imagePath, + user: {} + }; + } else { + giftedChatMessage = { + _id: id, + key: id, + createdAt: new Date(), + text: text, + system: true, + }; + } + + this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [giftedChatMessage])}); + } + + transferComplete(evt) { + console.log("Upload has finished", evt); + this.postChatSystemMessage('Upload has finished'); + } + + transferFailed(evt) { + console.log("An error occurred while transferring the file.", evt); + this.postChatSystemMessage('Upload failed') + } + + transferCanceled(evt) { + console.log("The transfer has been canceled by the user."); + this.postChatSystemMessage('Upload has canceled') + } + + async transferFile(msg) { + msg.metadata.preview = false; + this.props.sendMessage(msg.metadata.receiver.uri, msg, 'application/sylk-file-transfer'); + } + + async file2GiftedChat(fileObject) { + var id = uuid.v4(); + 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 filepath = fileObject.uri ? fileObject.uri : fileObject; + const basename = filepath.split('\\').pop().split('/').pop(); + let stats_filename = filepath.startsWith('file://') ? filepath.substr(7, filepath.length - 1) : filepath; + const { size } = await RNFetchBlob.fs.stat(stats_filename); + + let file_transfer = { 'path': filepath, + 'filename': basename, + 'filesize': fileObject.fileSize || size, + 'sender': {'uri': this.state.accountId}, + 'receiver': {'uri': uri}, + 'transfer_id': id, + 'direction': 'outgoing' + }; + + if (fileObject.preview) { + file_transfer.preview = fileObject.preview; + } + + if (fileObject.filetype) { + file_transfer.filetype = fileObject.filetype; + } + + let text = utils.beautyFileNameForBubble(file_transfer); + + let msg = { + _id: id, + key: id, + text: text, + metadata: file_transfer, + createdAt: new Date(), + direction: 'outgoing', + user: {} + } + + if (utils.isImage(basename)) { + msg.image = filepath; + } else if (utils.isAudio(basename)) { + msg.audio = filepath; + } else if (utils.isVideo(basename)) { + msg.video = filepath; + } + + return msg; + } + 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; } + onMessagePress(context, message) { + if (message.metadata && message.metadata.filename) { + let file_transfer = message.metadata; + if (!file_transfer.local_url) { + console.log('We have no local_url in metadata'); + this.props.downloadFunc(message.metadata); + return; + } + + RNFS.exists(file_transfer.local_url).then((exists) => { + if (exists) { + if (file_transfer.local_url.endsWith('.asc')) { + if (file_transfer.decryption_failed) { + this.onLongMessagePress(context, message); + } else { + this.props.decryptFunc(message.metadata); + } + } else { + this.onLongMessagePress(context, message); + //this.openFile(message) + } + } else { + console.log(file_transfer.local_url, 'does not exist localy'); + this.props.downloadFunc(message.metadata); + } + }); + } else { + this.onLongMessagePress(context, message); + } + } + + openFile(message) { + let file_transfer = message.metadata; + let file_path = file_transfer.local_url; + if (!file_path) { + console.log('Cannot open empty path'); + return; + } + + if (file_path.endsWith('.asc')) { + file_path = file_path.slice(0, -4); + console.log('Open decrypted file', file_path) + } else { + console.log('Open file', file_path) + } + + if (utils.isAudio(file_transfer.filename)) { +// this.startPlaying(file_path); + return; + } + + RNFS.exists(file_path).then((exists) => { + if (exists) { + FileViewer.open(file_path, { showOpenWithDialog: true }) + .then(() => { + // success + }) + .catch(error => { + // error + }); + } else { + console.log(file_path, 'does not exist'); + return; + } + }); + } + onLongMessagePress(context, currentMessage) { if (currentMessage && currentMessage.text) { let isSsiMessage = this.state.selectedContact && this.state.selectedContact.tags.indexOf('ssi') > -1; let options = [] - options.push('Copy'); + if (currentMessage.metadata && currentMessage.metadata.local_url) { + options.push('Open') + // + } else { + options.push('Copy') + } + if (!isSsiMessage) { options.push('Delete'); } - const showResend = currentMessage.failed; + let showResend = currentMessage.failed; + if (currentMessage.metadata && currentMessage.metadata.decryption_failed) { + showResend = false; + } if (this.state.targetUri.indexOf('@videoconference') === -1) { if (currentMessage.direction === 'outgoing') { if (showResend) { options.push('Resend') } } } if (currentMessage.pinned) { options.push('Unpin'); } else { if (!isSsiMessage) { options.push('Pin'); } } - options.push('Info'); + //options.push('Info'); if (!isSsiMessage) { - options.push('Share'); + if (currentMessage.metadata && currentMessage.metadata.decryption_failed) { + } else { + options.push('Share'); + } } if (currentMessage.local_url) { if (utils.isImage(currentMessage.local_url)) { options.push('Save'); } options.push('Open'); } + + if (currentMessage.metadata && currentMessage.metadata.filename) { + if (!currentMessage.metadata.filename.local_url || currentMessage.metadata.filename.decryption_failed) { + options.push('Download again'); + } else { + options.push('Download'); + } + } + options.push('Cancel'); let l = options.length - 1; context.actionSheet().showActionSheetWithOptions({options, l}, (buttonIndex) => { let action = options[buttonIndex]; if (action === 'Copy') { Clipboard.setString(currentMessage.text); } else if (action === 'Delete') { - this.props.deleteMessage(currentMessage._id, this.state.targetUri); + this.setState({showDeleteMessageModal: true, currentMessage: currentMessage}); } else if (action === 'Pin') { this.props.pinMessage(currentMessage._id); } else if (action === 'Unpin') { this.props.unpinMessage(currentMessage._id); } else if (action === 'Info') { this.setState({message: currentMessage, showMessageModal: true}); } else if (action === 'Share') { this.setState({message: currentMessage, showShareMessageModal: true}); } else if (action === 'Resend') { this.props.reSendMessage(currentMessage, this.state.targetUri); } else if (action === 'Save') { this.savePicture(currentMessage.local_url); + } else if (action.startsWith('Download')) { + this.props.downloadFunc(currentMessage.metadata, true); } else if (action === 'Open') { - FileViewer.open(currentMessage.local_url, { showOpenWithDialog: true }) + FileViewer.open(currentMessage.metadata.local_url, { showOpenWithDialog: true }) .then(() => { // success }) .catch(error => { - // error + console.log('Failed to open', currentMessage, error.message); }); } }); } }; + closeDeleteMessageModal() { + this.setState({showDeleteMessageModal: false}); + } + async hasAndroidPermission() { const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE; const hasPermission = await PermissionsAndroid.check(permission); if (hasPermission) { return true; } const status = await PermissionsAndroid.request(permission); return status === 'granted'; } async savePicture(file) { if (Platform.OS === "android" && !(await this.hasAndroidPermission())) { return; } file = 'file://' + file; console.log('Save to camera roll', file); CameraRoll.save(file); }; shouldUpdateMessage(props, nextProps) { return true; } toggleShareMessageModal() { this.setState({showShareMessageModal: !this.state.showShareMessageModal}); } renderMessageVideo(props){ const { currentMessage } = props; - return (null); return ( - -