diff --git a/android/VERSION_CODE b/android/VERSION_CODE index 871cae9..5f1a9f3 100644 --- a/android/VERSION_CODE +++ b/android/VERSION_CODE @@ -1 +1 @@ -323 \ No newline at end of file +324 \ No newline at end of file diff --git a/android/VERSION_NAME b/android/VERSION_NAME index 06eda28..9b7a431 100644 --- a/android/VERSION_NAME +++ b/android/VERSION_NAME @@ -1 +1 @@ -3.2.3 \ No newline at end of file +3.2.4 \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index a2bdc00..fad7eb0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,244 +1,244 @@ 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 323 - versionName "3.2.3" + versionCode 324 + versionName "3.2.4" 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(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 'androidx.activity:activity-ktx:1.2.0-alpha08' implementation 'androidx.fragment:fragment:1.3.0-alpha08' 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/app/components/ContactsListBox.js b/app/components/ContactsListBox.js index 579e022..3f26dcf 100644 --- a/app/components/ContactsListBox.js +++ b/app/components/ContactsListBox.js @@ -1,2346 +1,2350 @@ import React, { Component} from 'react'; import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; 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, Send, InputToolbar, MessageImage, Time} from 'react-native-gifted-chat' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' import MessageInfoModal from './MessageInfoModal'; import EditMessageModal from './EditMessageModal'; 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 { IconButton} from 'react-native-paper'; import ImageViewer from 'react-native-image-zoom-viewer'; import path from 'react-native-path'; 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'; const RNFS = require('react-native-fs'); import CameraRoll from "@react-native-community/cameraroll"; import {launchCamera, launchImageLibrary} from 'react-native-image-picker'; import AudioRecord from 'react-native-audio-record'; import FastImage from 'react-native-fast-image'; 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(); 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' }; // 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, message: null, inviteContacts: this.props.inviteContacts, shareToContacts: this.props.shareToContacts, selectMode: this.props.shareToContacts || this.props.inviteContacts, 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, 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, sourceContact: this.props.sourceContact } 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 && 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]; // remove duplicate messages no mater what renderMessages = renderMessages.filter((v,i,a)=>a.findIndex(v2=>['_id'].every(k=>v2[k] ===v[k]))===i); 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}); } } } } 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({isLandscape: nextProps.isLandscape, isTablet: nextProps.isTablet, chat: nextProps.chat, fontScale: nextProps.fontScale, filter: nextProps.filter, call: nextProps.call, password: nextProps.password, 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, messagesCategoryFilter: nextProps.messagesCategoryFilter, targetUri: nextProps.selectedContact ? nextProps.selectedContact.uri : nextProps.targetUri, keys: nextProps.keys, sourceContact: nextProps.sourceContact, isTexting: nextProps.isTexting, showDeleteMessageModal: nextProps.showDeleteMessageModal, selectMode: nextProps.shareToContacts || nextProps.inviteContacts }); 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: 2000, maxHeight: 2000, mediaType: 'mixed', quality:0.8, cameraType: 'front', formatAsMp4: true } const cameraAllowed = await this.props.requestCameraPermission(); if (cameraAllowed) { await launchCamera(options, this.cameraCallback); } } async _launchImageLibrary() { let options = {maxWidth: 2000, maxHeight: 2000, mediaType: 'mixed', formatAsMp4: true } await launchImageLibrary(options, this.libraryCallback); } async libraryCallback(result) { if (!result.assets || result.assets.length === 0) { return; } result.assets.forEach((asset) => { this.cameraCallback({assets: [asset]}); }); } async cameraCallback(result) { if (!result.assets || result.assets.length === 0) { return; } this.setState({scrollToBottom: true}); let asset = result.assets[0]; asset.preview = true; let msg = await this.props.file2GiftedChat(asset); let assetType = 'file'; if (msg.video) { assetType = 'movie'; } else if (msg.image) { assetType = 'photo'; } this.outgoingPendMessages[msg.metadata.transfer_id] = msg; this.setState({renderMessages: GiftedChat.append(this.state.renderMessages, [msg]), cameraAsset: msg, placeholder: 'Send ' + assetType + ' of ' + utils.beautySize(msg.metadata.filesize) }); } renderCustomActions = props => ( ) customInputToolbar = props => { return ( {this.renderComposer}} containerStyle={styles.chatInsideRightActionsContainer} /> ); }; chatInputChanged(text) { this.setState({texting: (text.length > 0)}) } async recordAudio() { const micAllowed = await this.props.requestMicPermission(); console.log('micAllowed', micAllowed); if (!micAllowed) { return; } if (!this.state.recording) { if (this.state.audioRecording) { this.deleteAudio(); } else { this.onStartRecord(); } } else { this.onStopRecord(); } } deleteAudio() { console.log('Delete audio'); this.setState({audioRecording: null, recording: false}); if (this.recordingStopTimer !== null) { clearTimeout(this.recordingStopTimer); this.recordingStopTimer = null; } } async onStartRecord () { console.log('Start recording...'); this.setState({recording: true}); this.recordingStopTimer = setTimeout(() => { this.stopRecording(); }, 20000); if (!AudioRecord) { AudioRecord.init(options); } try { AudioRecord.start(); } catch (e) { console.log(e.message); } }; stopRecording() { console.log('Stop recording...'); this.setState({recording: false}); if (this.recordingStopTimer !== null) { clearTimeout(this.recordingStopTimer); this.recordingStopTimer = null; } this.onRecording(false); this.onStopRecord(); } async onStopRecord () { console.log('Stop recording...'); const result = await AudioRecord.stop(); this.audioRecorded(result); this.setState({audioRecording: result}); }; resetContact() { this.stopRecordingTimer() this.outgoingPendMessages = {}; 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) => { let chatRightActionsContainer = Platform.OS === 'ios' ? styles.chatRightActionsContaineriOS : styles.chatRightActionsContainer; if (this.state.recording) { return ( ); } else { if (this.state.cameraAsset) { return ( ); } else if (this.state.audioRecording) { return ( ); } else { if (this.state.playing || (this.state.selectedContact && this.state.selectedContact.tags.indexOf('test') > -1)) { 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}); } closeEditMessageModal() { this.setState({showEditMessageModal: false, message: null}); } loadEarlierMessages() { //console.log('Load earlier messages...'); this.setState({scrollToBottom: false, isLoadingEarlier: true}); this.props.loadEarlierMessages(); } sendEditedMessage(message, text) { if (!this.state.selectedContact.uri) { return; } if (message.text === text) { return; } this.props.deleteMessage(message._id, this.state.selectedContact.uri); message._id = uuid.v4(); message.key = message._id; message.text = text; this.props.sendMessage(this.state.selectedContact.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() { if (this.state.cameraAsset && this.state.cameraAsset.metadata.transfer_id in this.outgoingPendMessages) { 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.props.file2GiftedChat(this.state.audioRecording); this.transferFile(msg); this.setState({audioRecording: null}); } } async _pickDocument() { const storageAllowed = await this.props.requestStoragePermission(); if (!storageAllowed) { return; } 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.props.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; } } }; renderMessageImageOld =(props) => { /* return( this.onMessagePress(context, props.currentMessage)}> ); */ return ( ) } renderMessageImage = (props: any) => { // https://github.com/FaridSafi/react-native-gifted-chat/issues/1950 const images = [ { url: props.currentMessage.image } ]; /* console.log('single press')} onLongPress={() => console.log('longpress')} style={{ backgroundColor: "transparent" }} > */ return ( ); }; 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'); } 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) { //console.log('File metadata', message.metadata); let file_transfer = message.metadata; if (!file_transfer.local_url) { if (!file_transfer.path) { // this was a local created upload, don't download as the file has not yet been uploaded this.props.downloadFunc(message.metadata, true); } return; } RNFS.exists(file_transfer.local_url).then((exists) => { if (exists) { if (file_transfer.local_url.endsWith('.asc')) { if (file_transfer.error) { this.onLongMessagePress(context, message); } else { this.props.decryptFunc(message.metadata); } } else { this.onLongMessagePress(context, message); //this.openFile(message) } } else { if (file_transfer.path) { // this was a local created upload, don't download as the file has not yet been uploaded this.onLongMessagePress(context, message); } else { this.props.downloadFunc(message.metadata, true); } } }); } 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.metadata) { currentMessage.metadata = {}; } //console.log('currentMessage', currentMessage); if (currentMessage && currentMessage.text) { let isSsiMessage = this.state.selectedContact && this.state.selectedContact.tags.indexOf('ssi') > -1; let options = [] if (currentMessage.metadata && !currentMessage.metadata.error) { if (!isSsiMessage && this.isMessageEditable(currentMessage)) { options.push('Edit'); } if (currentMessage.metadata && currentMessage.metadata.local_url) { options.push('Open') // } else { options.push('Copy'); } } if (!isSsiMessage) { options.push('Delete'); } let showResend = currentMessage.failed || (currentMessage.direction === 'outgoing' && !currentMessage.sent && !currentMessage.received && !currentMessage.pending); if (currentMessage.metadata && currentMessage.metadata.error) { 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 && !currentMessage.metadata.error) { options.push('Pin'); } } //options.push('Info'); if (!isSsiMessage && !currentMessage.metadata.error) { options.push('Forward'); } if (!isSsiMessage && !currentMessage.metadata.error) { options.push('Share'); } if (currentMessage.metadata && currentMessage.metadata.filename) { if (!currentMessage.metadata.filename.local_url || currentMessage.metadata.filename.error) { 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.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 === 'Edit') { this.setState({message: currentMessage, showEditMessageModal: true}); } else if (action.startsWith('Share')) { this.setState({message: currentMessage, showShareMessageModal: true}); } else if (action.startsWith('Forward')) { this.props.forwardMessageFunc(currentMessage, this.state.targetUri); } 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')) { console.log('Starting download...'); this.props.downloadFunc(currentMessage.metadata, true); } else if (action === 'Open') { FileViewer.open(currentMessage.metadata.local_url, { showOpenWithDialog: true }) .then(() => { // success }) .catch(error => { console.log('Failed to open', currentMessage, error.message); }); } }); } }; isMessageEditable(message) { if (message.direction === 'incoming') { return false; } if (message.image || message.audio || message.video) { return false; } if (message.metadata && message.metadata.filename) { return false; } return true; } 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 ( ); }; renderMessageAudio(props){ const { currentMessage } = props; let playAudioButtonStyle = Platform.OS === 'ios' ? styles.playAudioButtoniOS : styles.playAudioButton; if (currentMessage.metadata.playing === true) { return ( this.stopPlaying(currentMessage)} style={playAudioButtonStyle} icon="pause" /> ); } else { return ( this.startPlaying(currentMessage)} style={playAudioButtonStyle} icon="play" /> ); } }; videoError() { console.log('Video streaming error'); } onBuffer() { console.log('Video buffer error'); } // https://github.com/FaridSafi/react-native-gifted-chat/issues/571 // add view after bubble renderMessageText(props) { const { currentMessage } = props; if (currentMessage.video || currentMessage.audio) { return ( ); } else if (currentMessage.image) { return ( ); } else { return ( ); } }; renderTime = (props) => { const { currentMessage } = props; if (currentMessage.metadata && currentMessage.metadata.preview) { return null; } if (currentMessage.video) { return (