diff --git a/app/assets/styles/blink/_ContactCard.scss b/app/assets/styles/blink/_ContactCard.scss index af91b89..7dc6077 100644 --- a/app/assets/styles/blink/_ContactCard.scss +++ b/app/assets/styles/blink/_ContactCard.scss @@ -1,115 +1,155 @@ .containerPortrait { } .containerLandscape { } .cardPortraitContainer { margin-top: 0.5px; } .cardLandscapeContainer { flex: 1; margin-left: 1px; margin-top: 1px; border-radius: 2px; } .cardLandscapeTabletContainer { flex: 1; border: 1px; border-radius: 2px; } .cardPortraitTabletContainer { flex: 1; border: 1px; border-radius: 2px; } .rowContent { flex: 1; flex-direction: row; justify-content: space-between; } .cardContent { flex: 1; flex-direction: row; } .title { - padding-top:7px; font-size: 18px; line-height: 20px; flex: 1; } +.titlePaddingSmall { + padding-top:0px; +} + +.titlePadding { + padding-top:7px; +} + +.titlePaddingBig { + padding-top:14px; +} + .subtitle { font-size:14px; line-height: 20px; flex: 1; } .description { font-size:12px; flex: 1; } .avatarContent { - margin-top: 5px; + margin-top: 10px; } .gravatar { width: 50px; height: 50px; border-width: 2px; border-color: white; border-radius: 50px; } .mainContent { margin-left: 10px; } .rightContent { margin-top: 10px; margin-left: 60px; margin-right: 10px; align-items: flex-end; border: 0px; } .badgeTextStyle { font-size: 12px; } .badgeContainer { position: absolute; bottom: 10; right: 0; size: 30; } .selectedContact { margin-top: 15px; } +.timestamp { + margin-top: -5px; +} + .participants { margin-top: 10px; } .participant { font-size: 14px; } .buttonContainer { margin: 0 auto; margin-top:auto; } .button { border-radius: 2px; padding-left: 30px; padding-right: 30px; } + +.greenButtonContainer { + padding-right: 15px; +} + +.recordingLabel { + margin-top: 7px; +} + +.greenButton { + background-color: rgba(#6DAA63, .8); + margin-left: 0px; +} + +.audioButton { + background-color: white; +} + +.redButton { + background-color: red; +} + +.callButtons { + flex-direction: row; +} diff --git a/app/components/ChatActions.js b/app/components/ChatActions.js index b28c905..3e348ff 100644 --- a/app/components/ChatActions.js +++ b/app/components/ChatActions.js @@ -1,206 +1,204 @@ import PropTypes from 'prop-types' import React from 'react' 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() } } 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(); } } } 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 || (this.props.selectedContact && this.props.selectedContact.tags.indexOf('test') > -1)) { return () } 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() { let chatLeftActionsContainer = Platform.OS === 'ios' ? styles.chatLeftActionsContaineriOS : styles.chatLeftActionsContainer; return ( {this.renderIcon()} ) } } CustomActions.propTypes = { audioRecorded: PropTypes.func, onRecording: PropTypes.func, stopPlaying: PropTypes.func, options: PropTypes.object, texting: PropTypes.bool, sendingImage: PropTypes.bool, audioSendFinished: PropTypes.bool, selectedContact: PropTypes.object } export default CustomActions; diff --git a/app/components/ContactCard.js b/app/components/ContactCard.js index 59bb62f..587b5ab 100644 --- a/app/components/ContactCard.js +++ b/app/components/ContactCard.js @@ -1,432 +1,498 @@ import React, { Component, Fragment} from 'react'; import { View, SafeAreaView, FlatList } from 'react-native'; import { Badge } from 'react-native-elements' import autoBind from 'auto-bind'; import PropTypes from 'prop-types'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import { Card, IconButton, Button, Caption, Title, Subheading, List, Text, Menu} from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import uuid from 'react-native-uuid'; import styles from '../assets/styles/blink/_ContactCard.scss'; import UserIcon from './UserIcon'; import { GiftedChat } from 'react-native-gifted-chat' import {Gravatar, GravatarApi} from 'react-native-gravatar'; +import {Keyboard} from 'react-native'; import utils from '../utils'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } const Item = ({ nr, uri, name }) => ( {name !== uri? {name} ({uri}) : {uri} } ); const renderItem = ({ item }) => ( ); function isIp(ipaddress) { if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { return (true) } return (false) } class ContactCard extends Component { constructor(props) { super(props); autoBind(this); this.state = { id: this.props.contact.id, contact: this.props.contact, invitedParties: this.props.invitedParties, orientation: this.props.orientation, isTablet: this.props.isTablet, isLandscape: this.props.isLandscape, favorite: (this.props.contact.tags.indexOf('favorite') > -1)? true : false, blocked: (this.props.contact.tags.indexOf('blocked') > -1)? true : false, confirmRemoveFavorite: false, confirmPurgeChat: false, messages: this.props.messages, unread: this.props.unread, chat: this.props.chat, pinned: this.props.pinned, fontScale: this.props.fontScale, selectMode: this.props.selectMode } this.menuRef = React.createRef(); } UNSAFE_componentWillReceiveProps(nextProps) { this.setState({ id: nextProps.contact.id, contact: nextProps.contact, invitedParties: nextProps.invitedParties, isLandscape: nextProps.isLandscape, orientation: nextProps.orientation, favorite: (nextProps.contact.tags.indexOf('favorite') > -1)? true : false, blocked: (nextProps.contact.tags.indexOf('blocked') > -1)? true : false, chat: nextProps.chat, + recording: nextProps.recording, + audioRecording: nextProps.audioRecording, pinned: nextProps.pinned, messages: nextProps.messages, unread: nextProps.unread, fontScale: nextProps.fontScale, selectMode: nextProps.selectMode }); } shouldComponentUpdate(nextProps) { //https://medium.com/sanjagh/how-to-optimize-your-react-native-flatlist-946490c8c49b return true; } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } - toggleBlocked() { - this.props.toggleBlocked(this.state.contact.uri); - } - - setBlockedDomain() { - let newBlockedState = this.props.toggleBlocked('@' + this.state.contact.uri.split('@')[1]); - this.setState({blocked: newBlockedState}); - } - undo() { this.setState({confirmRemoveFavorite: false, confirmDeleteChat: false, action: null}); } onSendMessage(messages) { messages.forEach((message) => { // TODO send messages using account API }); this.setState({messages: GiftedChat.append(this.state.messages, messages)}); } setTargetUri(uri, contact) { this.props.setTargetUri(uri, this.state.contact); } renderChatComposer () { return null; } + startCall(event) { + event.preventDefault(); + Keyboard.dismiss(); + this.props.startCall(this.state.contact.uri, {audio: true, video: false}); + } + + recordAudio(event) { + event.preventDefault(); + Keyboard.dismiss(); + this.props.recordAudio(); + } + + sendAudio(event) { + event.preventDefault(); + Keyboard.dismiss(); + this.props.sendAudio(); + } + + deleteAudio(event) { + event.preventDefault(); + Keyboard.dismiss(); + this.props.deleteAudio(); + } + + startVideoCall(event) { + event.preventDefault(); + Keyboard.dismiss(); + this.props.startCall(this.state.contact.uri, {audio: true, video: true}); + } + render () { let showActions = this.state.contact.showActions && this.state.contact.tags.indexOf('test') === -1 && this.state.contact.tags !== ["synthetic"]; let tags = this.state.contact ? this.state.contact.tags : []; let uri = this.state.contact.uri; let username = uri.split('@')[0]; let domain = uri.split('@')[1]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); let name = this.state.contact.name; if (this.state.contact.organization) { name = name + ' - ' + this.state.contact.organization; } - let showBlockButton = !this.state.contact.conference && !this.state.chat; - let showBlockDomainButton = false; - let blockTextbutton = 'Block'; - let blockDomainTextbutton = 'Block domain'; let contact_ts = ''; let participantsData = []; - if (this.state.favorite) { - if (!this.state.blocked) { - showBlockButton = false; - } - } - - if (tags.indexOf('test') > -1) { - showBlockButton = false; - } - - if (name === 'Myself') { - showBlockButton = false; - } - - if (this.state.blocked) { - blockTextbutton = 'Unblock'; - } - let color = {}; let title = name || username; let subtitle = uri; let description = 'No calls or messages'; var todayStart = new Date(); todayStart.setHours(0,0,0,0); if (this.state.contact.timestamp) { description = moment(this.state.contact.timestamp).format('MMM D HH:mm'); if (this.state.contact.timestamp > todayStart) { contact_ts = moment(this.state.contact.timestamp).format('HH:mm'); } else { contact_ts = moment(this.state.contact.timestamp).format('DD-MM'); } } if (name === uri) { title = toTitleCase(username); } if (isPhoneNumber && isIp(domain)) { title = 'Tel ' + username; subtitle = 'From @' + domain; - showBlockDomainButton = true; } if (utils.isAnonymous(uri)) { //uri = 'anonymous@anonymous.invalid'; if (uri.indexOf('@guest.') > -1) { subtitle = 'From the Web'; } else { name = 'Anonymous'; } - showBlockDomainButton = true; - if (!this.state.blocked) { - showBlockButton = false; - } - blockDomainTextbutton = 'Block Web callers'; } if (!username || username.length === 0) { if (isIp(domain)) { title = 'IP domain'; } else if (domain.indexOf('guest.') > -1) { title = 'Calls from the Web'; } else { title = 'Domain'; } } let cardContainerClass = styles.portraitContainer; + let callButtonsEnabled = this.state.chat; if (this.state.isTablet) { cardContainerClass = (this.state.orientation === 'landscape') ? styles.cardLandscapeTabletContainer : styles.cardPortraitTabletContainer; } else { cardContainerClass = (this.state.orientation === 'landscape') ? styles.cardLandscapeContainer : styles.cardPortraitContainer; } let cardHeight = this.state.fontScale <= 1 ? 75 : 70; let duration; if (this.state.contact.tags.indexOf('history') > -1) { duration = moment.duration(this.state.contact.lastCallDuration, 'seconds').format('HH:mm:ss', {trim: false}); if (this.state.contact.direction === 'incoming' && this.state.contact.lastCallDuration === 0) { duration = 'missed'; } else if (this.state.contact.direction === 'outgoing' && this.state.contact.lastCallDuration === 0) { duration = 'cancelled'; } } if (this.state.contact.conference) { let participants = this.state.contact.participants; if (this.state.invitedParties && this.state.invitedParties.length > 0 ) { participants = this.state.invitedParties; } if (participants && participants.length > 0) { const p_text = participants.length > 1 ? 'participants' : 'participant'; subtitle = 'With ' + participants.length + ' ' + p_text; let i = 1; let contact_obj; let dn; let _item; participants.forEach((participant) => { contact_obj = this.findObjectByKey(this.props.contacts, 'uri', participant); dn = contact_obj ? contact_obj.name : participant; _item = {nr: i, id: uuid.v4(), uri: participant, name: dn}; participantsData.push(_item); i = i + 1; }); } else { subtitle = 'With no participants'; } } if (!name) { title = uri; } if (duration === 'missed') { subtitle = 'Last call missed from ', + uri; } else if (duration === 'cancelled') { subtitle = 'Last call cancelled to ' + uri; } else { subtitle = uri; } if (subtitle !== uri) { subtitle = subtitle + ' ' + uri; } if (title.indexOf('@videoconference') > -1) { title = name || username; } if (uri === 'anonymous@anonymous.invalid') { title = 'Anonymous caller'; } else { let isAnonymous = uri.indexOf('@guest.') > -1; if (isAnonymous) { title = title + ' (Web call)'; } } if (duration && duration !== "00:00:00") { let media = 'Audio call'; if (this.state.contact.lastCallMedia.indexOf('video') > -1) { media = 'Video call'; } description = description + ' (' + media + ' ' + duration + ')'; } + let greenButtonClass = Platform.OS === 'ios' ? styles.greenButtoniOS : styles.greenButton; + const container = this.state.isLandscape ? styles.containerLandscape : styles.containerPortrait; const chatContainer = this.state.isLandscape ? styles.chatLandscapeContainer : styles.chatPortraitContainer; let showSubtitle = (showActions || this.state.isTablet || !description); let label = this.state.contact.label ? (" (" + this.state.contact.label + ")" ) : ''; if (this.state.contact.lastMessage) { subtitle = this.state.contact.lastMessage.split("\n")[0]; //description = description + ': ' + this.state.contact.lastMessage; } else { subtitle = subtitle + label; } let unread = (this.state.contact && this.state.contact.unread && !this.state.selectMode) ? this.state.contact.unread.length : 0; let selectCircle = this.state.contact.selected ? 'check-circle' : 'circle-outline'; + let recordIcon = this.state.recording ? 'pause' : 'microphone'; + let recordStyle = this.state.recording ? styles.greenButton : styles.greenButton; + + if (this.state.audioRecording) { + recordIcon = 'delete'; + recordStyle = styles.redButton; + } + + let titlePadding = styles.titlePadding; + + if (this.state.fontScale < 1) { + titlePadding = styles.titlePaddingBig; + } else if (this.state.fontScale > 1.2) { + titlePadding = styles.titlePaddingSmall; + } + return ( {this.setTargetUri(uri, this.state.contact)}} > { this.state.contact.photo || ! this.state.contact.email ? : } - {title} - {subtitle} - {this.state.fontScale <= 1 ? + {title} + {callButtonsEnabled ? + + + + + {!this.state.recording && !this.state.audioRecording ? + + + + : null + } + + { this.state.recording && !this.state.audioRecording ? + Recording audio... + : null + } + + {!this.state.recording && !this.state.audioRecording ? + + + + : null} + + {!this.state.recording && this.state.audioRecording ? + + + + : null + } + + + : {subtitle}} + + {this.state.fontScale <= 1 && !callButtonsEnabled ? {this.state.contact.direction ? : null} {description} : null} {participantsData && participantsData.length && showActions && false? item.id} key={item => item.id} /> : null} + { this.state.selectMode ? : - {contact_ts} + {contact_ts} } + {unread ? : null } + - - {showActions && false ? - - - {showBlockButton? : null} - {showBlockDomainButton? : null} - - - : null} ); } } ContactCard.propTypes = { id : PropTypes.string, contact : PropTypes.object, setTargetUri : PropTypes.func, chat : PropTypes.bool, orientation : PropTypes.string, isTablet : PropTypes.bool, isLandscape : PropTypes.bool, contacts : PropTypes.array, defaultDomain : PropTypes.string, accountId : PropTypes.string, favoriteUris : PropTypes.array, messages : PropTypes.array, pinned : PropTypes.bool, unread : PropTypes.array, - toggleBlocked : PropTypes.func, sendPublicKey : PropTypes.func, fontScale : PropTypes.number, - selectMode : PropTypes.bool + selectMode : PropTypes.bool, + startCall : PropTypes.func, + recordAudio : PropTypes.func, + sendAudio : PropTypes.func, + deleteAudio : PropTypes.func, + recording : PropTypes.bool, + audioRecording : PropTypes.string }; export default ContactCard; diff --git a/app/components/ContactsListBox.js b/app/components/ContactsListBox.js index 59a318c..0181873 100644 --- a/app/components/ContactsListBox.js +++ b/app/components/ContactsListBox.js @@ -1,2298 +1,2363 @@ 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 RNFetchBlob from "rn-fetch-blob"; import { IconButton} from 'react-native-paper'; import ImageViewer from 'react-native-image-zoom-viewer'; import fileType from 'react-native-file-type'; 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 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' +}; + +AudioRecord.init(options); + // 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 } 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, 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.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) }); } renderMessageImage =(props) => { /* return( this.onMessagePress(context, props.currentMessage)}> ); */ return ( ) } renderCustomActions = props => ( ) customInputToolbar = props => { return ( {this.renderComposer}} containerStyle={styles.chatInsideRightActionsContainer} /> ); }; chatInputChanged(text) { this.setState({texting: (text.length > 0)}) } + recordAudio() { + 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); + AudioRecord.start(); + }; + + 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.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; let basename = fileObject.fileName || filepath.split('\\').pop().split('/').pop(); basename = basename.replace(/\s|:/g, '_'); let file_transfer = { 'path': filepath, 'filename': basename, 'sender': {'uri': this.state.accountId}, 'receiver': {'uri': uri}, 'transfer_id': id, 'direction': 'outgoing' }; if (filepath.startsWith('content://')) { // on android we must copy this file early const localPath = RNFS.DocumentDirectoryPath + "/" + this.state.accountId + "/" + uri + "/" + id + "/" + basename; const dirname = path.dirname(localPath); await RNFS.mkdir(dirname); console.log('Copy', filepath, localPath); await RNFS.copyFile(filepath, localPath); filepath = localPath; file_transfer.local_url = localPath; } let stats_filename = filepath.startsWith('file://') ? filepath.substr(7, filepath.length - 1) : filepath; const { size } = await RNFetchBlob.fs.stat(stats_filename); file_transfer.filesize = fileObject.fileSize || size; if (fileObject.preview) { file_transfer.preview = fileObject.preview; } if (fileObject.duration) { file_transfer.duration = fileObject.duration; } if (fileObject.fileType) { file_transfer.filetype = fileObject.fileType; } else { try { let mime = await fileType(filepath); if (mime.mime) { file_transfer.filetype = mime.mime; } } catch (e) { console.log('Error getting mime type', e.message); } } 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) || file_transfer.duration) { 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) { //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 === 'decryption failed') { 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 metadata', currentMessage.metadata); 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; if (currentMessage.metadata && currentMessage.metadata.error === '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 && !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.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.error === 'decryption failed') { if (currentMessage.metadata.direction !== 'outgoing') { 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.image || currentMessage.audio) { return ( ); } else { return ( ); } }; renderTime = (props) => { const { currentMessage } = props; if (currentMessage.metadata && currentMessage.metadata.preview) { return null; } if (currentMessage.video) { return (