diff --git a/android/app/build.gradle b/android/app/build.gradle
index 40ff328e..bca8bdd9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -108,6 +108,7 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation(platform("com.google.firebase:firebase-bom:32.6.0"))
+ implementation project(':react-native-share')
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 09316e07..d926ba5e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -27,6 +27,12 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/kit_box/MainApplication.java b/android/app/src/main/java/com/kit_box/MainApplication.java
index 5c781724..3d4f46c9 100644
--- a/android/app/src/main/java/com/kit_box/MainApplication.java
+++ b/android/app/src/main/java/com/kit_box/MainApplication.java
@@ -9,6 +9,8 @@
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import java.util.List;
+import cl.json.RNSharePackage;
+import cl.json.ShareApplication;
public class MainApplication extends Application implements ReactApplication {
diff --git a/android/build.gradle b/android/build.gradle
index a2a87f4b..c3c5d0fb 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,6 +6,8 @@ buildscript {
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
+
+ // kotlinVersion = '1.5.0'
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
@@ -18,5 +20,6 @@ buildscript {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath ('com.google.gms:google-services:4.4.0')
+ // classpath ("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
diff --git a/android/settings.gradle b/android/settings.gradle
index b52bc8ba..def05afb 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -2,3 +2,7 @@ rootProject.name = 'kit_box'
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
+include ':react-native-audio-recorder-player'
+project(':react-native-audio-recorder-player').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio-recorder-player/android')
+include ':react-native-share'
+project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
\ No newline at end of file
diff --git a/package.json b/package.json
index 7bb6f538..60da166d 100644
--- a/package.json
+++ b/package.json
@@ -36,10 +36,12 @@
"patch-package": "^8.0.0",
"react": "18.2.0",
"react-native": "0.72.6",
+ "react-native-audio-recorder-player": "^3.6.5",
"react-native-blob-util": "^0.19.2",
"react-native-calendars": "^1.1302.0",
"react-native-document-picker": "^9.0.1",
"react-native-dotenv": "^3.4.9",
+ "react-native-fs": "^2.20.0",
"react-native-gesture-bottom-sheet": "^1.1.0",
"react-native-gesture-handler": "^2.14.0",
"react-native-gifted-chat": "^2.4.0",
@@ -58,8 +60,10 @@
"react-native-safe-area-context": "^4.7.4",
"react-native-screens": "^3.27.0",
"react-native-select-dropdown": "^3.4.0",
+ "react-native-share": "^10.0.2",
"react-native-size-matters": "^0.4.2",
"react-native-slider": "^0.11.0",
+ "react-native-sound": "^0.11.2",
"react-native-splash-screen": "^3.3.0",
"react-native-svg": "^13.14.0",
"react-native-svg-transformer": "^1.1.0",
@@ -70,6 +74,7 @@
"react-native-twilio-video-webrtc": "^3.2.0",
"react-native-vector-icons": "^10.0.1",
"react-native-video": "^5.2.1",
+ "react-native-video-controls": "^2.8.1",
"react-native-video-player": "^0.14.0",
"react-native-vision-camera": "^3.6.10",
"rn-bottom-drawer": "^1.4.3",
diff --git a/src/Assets/images/video.png b/src/Assets/images/video.png
new file mode 100644
index 00000000..a90e2b57
Binary files /dev/null and b/src/Assets/images/video.png differ
diff --git a/src/Components/Chat/FileTransfer.jsx b/src/Components/Chat/FileTransfer.jsx
index 467d6de1..863945c0 100644
--- a/src/Components/Chat/FileTransfer.jsx
+++ b/src/Components/Chat/FileTransfer.jsx
@@ -23,11 +23,11 @@ const FileTransfer = props => {
{props.isFooter ? (
''
@@ -57,6 +57,7 @@ const styles = StyleSheet.create({
lineHeight: 20,
marginLeft: 5,
marginRight: 5,
+ maxWidth: moderateScale(175),
},
textType: {
color: 'black',
diff --git a/src/Components/Chat/List.jsx b/src/Components/Chat/List.jsx
index 4ad33bf9..a75cac1f 100644
--- a/src/Components/Chat/List.jsx
+++ b/src/Components/Chat/List.jsx
@@ -8,14 +8,19 @@ import Dot from 'react-native-vector-icons/Octicons';
import Check from 'react-native-vector-icons/AntDesign';
import {useNavigation} from '@react-navigation/core';
-const Lists = ({items, groupContact}) => {
+const Lists = ({items, groupContact, handleGroupSelection}) => {
const Item = items.item;
const navigation = useNavigation();
return (
navigation.navigate('ChatRoom', {Item})}>
+ onLongPress={() => handleGroupSelection(Item)}
+ onPress={() =>
+ groupContact?.length > 0
+ ? handleGroupSelection(Item)
+ : navigation.navigate('ChatRoom', {Item})
+ }>
(
<>
@@ -112,6 +117,7 @@ export default Lists;
const styles = StyleSheet.create({
container: {
+ flex: 1,
backgroundColor: colors.WHITE,
marginBottom: moderateScale(-12),
},
diff --git a/src/Components/Chat/ViewFile.jsx b/src/Components/Chat/ViewFile.jsx
index f91e8918..795fdf14 100644
--- a/src/Components/Chat/ViewFile.jsx
+++ b/src/Components/Chat/ViewFile.jsx
@@ -2,31 +2,41 @@ import React, {useState} from 'react';
import {Modal, Portal, Text, TouchableRipple, Icon} from 'react-native-paper';
import {moderateScale} from 'react-native-size-matters';
import Pdf from 'react-native-pdf';
-import {StyleSheet, View} from 'react-native';
+import {Image, StyleSheet, View} from 'react-native';
import {colors} from '../../Utils/colors';
import {fonts} from '../../Utils/fonts';
-const ViewFile = ({props, visible, onClose}) => {
- const filePath = props.currentMessage.file.url;
+const ViewFile = ({props, visible, onClose, isImage, isVideo}) => {
+ const filePath = props.file.url || props.image;
var name = '';
if (filePath !== undefined) {
name = filePath.split('/').pop();
}
- const [url, setUrl] = useState(props.currentMessage.file.url);
+
return (
{name}
-
+ {!isImage && !isVideo && (
+
+ )}
+ {isImage && (
+
+ )}
-
+
@@ -45,7 +55,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
position: 'absolute',
borderColor: 'black',
- left: '85%',
+ left: '88%',
top: moderateScale(-30),
},
textBtn: {
@@ -63,8 +73,13 @@ const styles = StyleSheet.create({
// position: 'absolute',
paddingTop: moderateScale(50),
paddingLeft: moderateScale(20),
- fontFamily: fonts.BOLD,
- fontSize: moderateScale(20),
- backgroundColor: colors.GRAY,
+ fontFamily: fonts.MEDIUM,
+ fontSize: moderateScale(12),
+ maxWidth: moderateScale(320),
+ },
+
+ viewImage: {
+ alignSelf: 'center',
+ resizeMode: 'contain',
},
});
diff --git a/src/Components/Header/HeaderWithBackaction.jsx b/src/Components/Header/HeaderWithBackaction.jsx
index 708c40b1..21695b0f 100644
--- a/src/Components/Header/HeaderWithBackaction.jsx
+++ b/src/Components/Header/HeaderWithBackaction.jsx
@@ -9,7 +9,7 @@ import MenuPopup from '../Menu/Menu';
import {useNavigation} from '@react-navigation/native';
import Icon from 'react-native-vector-icons/Feather';
-const HeaderWithBackaction = ({title, openMenu, isChat, profile}) => {
+const HeaderWithBackaction = ({title, openMenu, isChat, profile_pic}) => {
const navigation = useNavigation();
const _goBack = () => navigation.navigate('ChatList');
@@ -18,6 +18,8 @@ const HeaderWithBackaction = ({title, openMenu, isChat, profile}) => {
const _handleMore = () => openMenu();
+ console.log(profile_pic, ' pp');
+
return (
{
size={20}
onPress={() => navigation.goBack()}
/>
- {isChat && }
+ {isChat && }
{
size={25}
onPress={_handleMore}
/> */}
- {/* */}
+
>
);
@@ -65,7 +65,6 @@ const styles = StyleSheet.create({
title: {
fontFamily: fonts.BOLD,
fontSize: moderateScale(22),
- fontWeight: 'bold',
color: colors.WHITE,
},
});
diff --git a/src/Components/Menu/Menu.jsx b/src/Components/Menu/Menu.jsx
index ab98b655..b080c424 100644
--- a/src/Components/Menu/Menu.jsx
+++ b/src/Components/Menu/Menu.jsx
@@ -22,9 +22,19 @@ const MenuPopup = props => {
}>
- {}} title="Item 1" />
- {}} title="Item 2" />
-
+ {props.check && (
+ <>
+ {
+ navigation.navigate('Groups');
+ setVisible(false);
+ }}
+ title="Create Group"
+ />
+ {}} title="Settings" />
+
+ >
+ )}
navigation.replace('Login')} title="Logout" />
);
diff --git a/src/Components/Modal/PopupModal.jsx b/src/Components/Modal/PopupModal.jsx
index 96af5af2..10321967 100644
--- a/src/Components/Modal/PopupModal.jsx
+++ b/src/Components/Modal/PopupModal.jsx
@@ -14,16 +14,37 @@ import {colors} from '../../Utils/colors';
import InputField from '../TextInput/InputField';
import Cancel from 'react-native-vector-icons/MaterialIcons';
import CustomButton from '../Button/CustomButton';
+import {dateFormatter, groupChatList} from '../../Data/GroupChatList';
+import {user_1} from '../../Data/ChatRoom';
+import {useNavigation} from '@react-navigation/native';
const PopupModal = props => {
const [visible, setVisible] = React.useState(false);
+ const [groupName, setGroupName] = React.useState('');
+ const [groupDescription, setGroupDescription] = React.useState('');
+ const navigation = useNavigation();
const showModal = () => setVisible(true);
const hideModal = () => setVisible(false);
- const containerStyle = {backgroundColor: 'white', padding: 20};
+
+ const handleGroupCreation = item => {
+ const data = {
+ user_id: groupChatList.length + 1,
+ name: item,
+ last_msg: groupDescription,
+ modified_date: dateFormatter(user_1[0].createdAt),
+ profile_pic:
+ 'data:image/jpeg;base64,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',
+ msg_read: false,
+ active: true,
+ last_seen: '',
+ };
+ groupChatList.unshift(data);
+ navigation.replace('Groups');
+ };
return (
- <>
+
{
Selected Contacts: {props.selectedCount}
-
+ setGroupName(val)}
+ />
-
+ setGroupDescription(val)}
+ />
-
+ handleGroupCreation(groupName)}
+ />
-
+
Create Group
- >
+
);
};
const styles = StyleSheet.create({
container: {
- width: '90%',
- height: '50%',
+ width: moderateScale(300),
+ height: moderateScale(350),
alignSelf: 'center',
backgroundColor: colors.WHITE,
padding: moderateScale(20),
@@ -71,7 +113,7 @@ const styles = StyleSheet.create({
},
btnText: {
- top: moderateScale(20),
+ top: moderateScale(25),
marginRight: moderateScale(10),
alignSelf: 'flex-end',
},
diff --git a/src/Components/Video/ChatVideoPlayer.jsx b/src/Components/Video/ChatVideoPlayer.jsx
new file mode 100644
index 00000000..74b83e1d
--- /dev/null
+++ b/src/Components/Video/ChatVideoPlayer.jsx
@@ -0,0 +1,26 @@
+import {View, Text, StyleSheet} from 'react-native';
+import React from 'react';
+import VideoPlayer from 'react-native-video-controls';
+import {useNavigation, useRoute} from '@react-navigation/native';
+
+const ChatVideoPlayer = () => {
+ const navigation = useNavigation();
+ const route = useRoute();
+
+ return (
+
+ navigation.goBack()}
+ />
+
+ );
+};
+
+export default ChatVideoPlayer;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/src/Containers/Chats/Chat.jsx b/src/Containers/Chats/Chat.jsx
index c3616aea..9df2fbbc 100644
--- a/src/Containers/Chats/Chat.jsx
+++ b/src/Containers/Chats/Chat.jsx
@@ -1,56 +1,92 @@
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
- Image,
- Button,
- Platform,
-} from 'react-native';
-import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
+import {View, Text, StyleSheet, TouchableOpacity, Image} from 'react-native';
+import React, {useCallback, useEffect, useState} from 'react';
import {colors} from '../../Utils/colors';
import {GiftedChat, Bubble, Send, InputToolbar} from 'react-native-gifted-chat';
import HeaderWithBackaction from '../../Components/Header/HeaderWithBackaction';
-import {user_1, user_2, user_3} from '../../Data/ChatRoom';
-import {useRoute} from '@react-navigation/native';
+import {useNavigation, useRoute} from '@react-navigation/native';
import {Icon} from 'react-native-paper';
import {moderateScale} from 'react-native-size-matters';
-import DocumentPicker from 'react-native-document-picker';
import FileTransfer from '../../Components/Chat/FileTransfer';
import ViewFile from '../../Components/Chat/ViewFile';
import Plus from 'react-native-vector-icons/AntDesign';
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import ChatLogic from '../../Functions/Chat/Chat';
+import Mic from 'react-native-vector-icons/Entypo';
+import {fonts} from '../../Utils/fonts';
+import PlayPause from 'react-native-vector-icons/AntDesign';
+import Delete from 'react-native-vector-icons/MaterialCommunityIcons';
+import Slider from '@react-native-community/slider';
+import TrackPlayer from 'react-native-track-player';
+import _ from 'lodash';
+import Wave from 'react-native-vector-icons/MaterialIcons';
+import Share from 'react-native-share';
const Chat = () => {
const {
messages,
- setMessages,
attachments,
setAttachments,
fileVisible,
setFileVisible,
- isMenuOpen,
openMenu,
closeMenu,
onSend,
pickDocument,
- navigation,
currentTime,
+ audioURL,
+ recordingActive,
+ time,
+ StartRecording,
+ DeleteRecording,
+ playingAudio,
+ setPlayingAudio,
+ Format,
+ position,
+ duration,
+ currentAudioId,
+ TogglePlayback,
+ onSliderValueChange,
} = ChatLogic();
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ if (position == duration) {
+ TrackPlayer.seekTo(0);
+ TrackPlayer.pause();
+ setPlayingAudio(false);
+ }
+ }, [position, duration]);
+
const route = useRoute();
const renderSend = props => {
return (
-
-
-
-
-
+ {!recordingActive && (
+
+
+
+ )}
+
+
);
@@ -64,7 +100,9 @@ const Chat = () => {
style={{
...styles.fileContainer,
backgroundColor:
- props.currentMessage.user._id === 2 ? '#2e64e5' : '#efefef',
+ props.currentMessage.user._id === 2
+ ? colors.APP_PRIMARY
+ : '#efefef',
borderBottomLeftRadius:
props.currentMessage.user._id === 2 ? 15 : 5,
borderBottomRightRadius:
@@ -72,7 +110,7 @@ const Chat = () => {
}}
onPress={() => setFileVisible(true)}>
setFileVisible(false)}
/>
@@ -98,13 +136,197 @@ const Chat = () => {
);
+ } else if (currentMessage.audio && currentMessage.audio.url) {
+ const audioDurationString = currentMessage.audio.duration || '0:00';
+ const [minutes, seconds] = audioDurationString.split(':');
+ const audioDuration = parseInt(minutes, 10) * 60 + parseInt(seconds, 10);
+
+ return (
+
+ TogglePlayback(currentMessage)}>
+
+
+
+
+
+ {currentAudioId === currentMessage._id && playingAudio
+ ? Format(position)
+ : currentMessage.audio.duration}
+
+
+
+
+ {currentMessage.text}
+
+
+ {currentTime}
+
+
+
+ );
+ } else if (currentMessage.image && currentMessage.image !== '') {
+ return (
+
+
+ setFileVisible(true)}>
+ setFileVisible(false)}
+ isImage={true}
+ />
+
+
+
+
+ {currentMessage.text}
+
+
+ {currentTime}
+
+
+
+
+ );
+ } else if (currentMessage.video && currentMessage.video.url !== '') {
+ const handleVideoClick = async () => {
+ try {
+ const options = {
+ dialogTitle: 'Choose a video player',
+ };
+ await Share.open(
+ {url: currentMessage.video.url, failOnCancel: false},
+ options,
+ );
+ } catch (error) {
+ console.error('Error sharing video:', error);
+ }
+ };
+
+ return (
+
+
+
+ navigation.navigate('ChatVideoPlayer', {
+ url: currentMessage.video.url,
+ })
+ }>
+
+
+
+
+ {currentMessage.text}
+
+
+ {currentTime}
+
+
+
+
+ );
}
+
return (
{
const modifiedProps = {...props};
if (props.text.length === 0 && attachments[0] !== undefined) {
modifiedProps.text = ' ';
+ } else if (audioURL !== '') {
+ modifiedProps.text = ' ';
}
return (
-
+
+
+
+
+
+
);
};
@@ -182,6 +416,29 @@ const Chat = () => {
isFooter={true}
/>
)}
+ {attachment.type === 'video' && (
+
+
+ setImagePath('')}
+ style={styles.buttonFooterChat}>
+ X
+
+
+ )}
{
const updatedAttachments = [...attachments];
@@ -195,10 +452,39 @@ const Chat = () => {
))}
);
- } else {
- return null;
+ } else if (recordingActive) {
+ return (
+
+
+
+
+ Recording Voice {' '} {time}
+
+
+ {/* toggleRecording()}>
+
+ */}
+ audioURL !== '' && DeleteRecording()}>
+
+
+
+ );
}
- }, [attachments]);
+ }, [attachments, time, recordingActive, audioURL]);
return (
@@ -209,6 +495,7 @@ const Chat = () => {
openMenu={openMenu}
closeMenu={closeMenu}
isChat={true}
+ profile_pic={route.params?.Item.profile_pic}
/>
{
scrollToBottomComponent={scrollToBottomComponent}
renderChatFooter={renderChatFooter}
renderInputToolbar={renderInputToolbar}
- messagesContainerStyle={{
- paddingBottom:
- Platform.OS === 'ios' ? moderateScale(40) : moderateScale(15),
- }}
+ messagesContainerStyle={styles.messagesContainer}
+ placeholder="Say something..."
/>
);
@@ -241,8 +526,7 @@ const styles = StyleSheet.create({
shareContainer: {
flexDirection: 'row',
marginBottom: 12,
- marginRight: moderateScale(10),
- width: moderateScale(70),
+ marginRight: moderateScale(-40),
height: moderateScale(40),
justifyContent: 'space-between',
},
@@ -317,6 +601,13 @@ const styles = StyleSheet.create({
fileShare: {
marginTop: moderateScale(16),
+ marginRight: moderateScale(10),
+ borderWidth: 1,
+ borderColor: colors.TRANSPARENT,
+ width: moderateScale(40),
+ height: moderateScale(30),
+ // justifyContent: 'center',
+ // alignItems: 'center',
},
timeText: {
@@ -329,15 +620,110 @@ const styles = StyleSheet.create({
input: {
borderRadius: moderateScale(30),
backgroundColor: colors.GRAY10,
- marginBottom: moderateScale(10),
borderTopWidth: 0,
marginHorizontal: moderateScale(10),
- marginRight: moderateScale(4),
- alignItems: 'center',
+ marginRight: moderateScale(60),
+ width: moderateScale(290),
+ paddingBottom: moderateScale(5),
},
messagesContainer: {
paddingBottom: moderateScale(15),
+ fontFamily: fonts.MEDIUM,
+ },
+
+ microphone: {
+ borderWidth: 1,
+ justifyContent: 'center',
+ alignSelf: 'flex-end',
+ alignItems: 'center',
+ marginRight: moderateScale(10),
+ marginTop: moderateScale(-5),
+ backgroundColor: colors.APP_PRIMARY,
+ width: moderateScale(50),
+ height: moderateScale(52),
+ borderRadius: moderateScale(30),
+ },
+
+ inputWrapper: {
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ bottom: moderateScale(10),
+ },
+
+ recordContainer: {
+ width: moderateScale(300),
+ height: moderateScale(50),
+ borderRadius: moderateScale(30),
+ backgroundColor: colors.GRAY10,
+ alignSelf: 'flex-start',
+ justifyContent: 'center',
+ marginHorizontal: moderateScale(10),
+ bottom: moderateScale(15),
+ paddingHorizontal: moderateScale(15),
+ },
+
+ audioFooter: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ elevation: 2,
+ borderBottomLeftRadius: moderateScale(10),
+ borderBottomRightRadius: moderateScale(10),
+ borderTopLeftRadius: 10,
+ borderTopRightRadius: 10,
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.18)',
+ height: moderateScale(60),
+ marginVertical: moderateScale(10),
+ marginHorizontal: moderateScale(10),
+ paddingTop: moderateScale(5),
+ paddingHorizontal: moderateScale(10),
+ },
+
+ recorderText: {
+ fontFamily: fonts.BOLD,
+ fontSize: moderateScale(14),
+ marginHorizontal: moderateScale(10),
+ },
+
+ audioInnerContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+
+ audioBubble: {
+ flexDirection: 'row',
+ padding: moderateScale(5),
+ justifyContent: 'center',
+ alignItems: 'center',
+ maxWidth: moderateScale(250),
+ },
+
+ audioText: {
+ fontFamily: fonts.BOLD,
+ color: colors.WHITE,
+ paddingHorizontal: moderateScale(10),
+ },
+
+ sliderContainer: {
+ paddingHorizontal: moderateScale(5),
+ marginBottom: moderateScale(10),
+ },
+
+ PlayPauseButton: {
+ paddingTop: moderateScale(1),
+ paddingHorizontal: moderateScale(1),
+ },
+
+ renderImage: {
+ borderRadius: moderateScale(10),
+ },
+
+ timeContainer: {
+ marginTop: moderateScale(10),
+ marginBottom: moderateScale(-5),
+ marginRight: moderateScale(-5),
},
});
diff --git a/src/Containers/Chats/ChatList.jsx b/src/Containers/Chats/ChatList.jsx
index 16766412..6b963ccf 100644
--- a/src/Containers/Chats/ChatList.jsx
+++ b/src/Containers/Chats/ChatList.jsx
@@ -1,5 +1,5 @@
import {View, FlatList, StyleSheet} from 'react-native';
-import React from 'react';
+import React, {useEffect} from 'react';
import {colors} from '../../Utils/colors';
import List from '../../Components/Chat/List';
import HeaderWithSearch from '../../Components/Header/HeaderWithSearch';
@@ -7,7 +7,7 @@ import {Searchbar} from 'react-native-paper';
import Cancel from 'react-native-vector-icons/MaterialIcons';
import {moderateScale} from 'react-native-size-matters';
import ChatListLogic from '../../Functions/Chat/ChatList';
-import {chatList} from '../../Data/ChatList';
+import TrackPlayer from 'react-native-track-player';
const ChatList = () => {
const {
@@ -23,6 +23,14 @@ const ChatList = () => {
navigation,
} = ChatListLogic();
+ useEffect(() => {
+ const setupPlayer = async () => {
+ await TrackPlayer.setupPlayer();
+ console.log('Player is initialized');
+ };
+ setupPlayer();
+ }, []);
+
return (
{
setSearch={setSearch}
openDrawer={openDrawer}
setOpenDrawer={setOpenDrawer}
+ isChatList={true}
/>
{search && (
{
selected,
setSelected,
handleSearch,
- HandleGroupSelection,
+ handleGroupSelection,
navigation,
} = ContactsLogic();
@@ -47,13 +47,13 @@ const Contacts = () => {
onPress={() => {
setSearch(false);
setSearchQuery('');
- setFilteredContacts(chatList);
+ // setFilteredContacts(chatList);
}}
/>
)}
style={styles.searchBar}
/>
-
+
{isGroupSelection && (
@@ -64,18 +64,12 @@ const Contacts = () => {
(
- HandleGroupSelection(item.item)}
- onPress={() =>
- groupContact.length > 0 && HandleGroupSelection(item.item)
- }>
-
-
+
)}
/>
diff --git a/src/Containers/Chats/GroupList.jsx b/src/Containers/Chats/GroupList.jsx
index d006fbc3..ea5d324c 100644
--- a/src/Containers/Chats/GroupList.jsx
+++ b/src/Containers/Chats/GroupList.jsx
@@ -42,14 +42,25 @@ const GroupListUI = () => {
style={styles.searchBar}
/>
)}
-
}
- />
+
+
}
+ />
+
navigation.navigate('Contacts')}>
-
+
);
@@ -78,9 +89,10 @@ const styles = StyleSheet.create({
addIcon: {
position: 'absolute',
- alignSelf: 'flex-end',
- right: moderateScale(20),
- top: '85%',
+ width: moderateScale(55),
+ left: moderateScale(280),
+ bottom: moderateScale(50),
+ borderRadius: moderateScale(40),
},
});
diff --git a/src/Containers/VideoPlayer/VideoPlayer.jsx b/src/Containers/VideoPlayer/VideoPlayer.jsx
index aa453d13..229438e9 100644
--- a/src/Containers/VideoPlayer/VideoPlayer.jsx
+++ b/src/Containers/VideoPlayer/VideoPlayer.jsx
@@ -171,8 +171,9 @@ const VideoPlayer = () => {
value={progress.currentTime}
minimumValue={0}
maximumValue={progress.seekableDuration}
- minimumTrackTintColor="#FFFFFF"
- maximumTrackTintColor="#fff"
+ minimumTrackTintColor={colors.WHITE}
+ maximumTrackTintColor={colors.WHITE}
+ thumbTintColor={colors.WHITE}
onValueChange={x => {
ref.current.seek(x);
setProgress({...progress, currentTime: x});
diff --git a/src/Data/GroupChatList.js b/src/Data/GroupChatList.js
index 8e235ee1..5628cbef 100644
--- a/src/Data/GroupChatList.js
+++ b/src/Data/GroupChatList.js
@@ -1,6 +1,6 @@
import {user_1, user_2, user_3} from './ChatRoom';
-const dateFormatter = timestamp => {
+export const dateFormatter = timestamp => {
const date = new Date(timestamp);
const today = new Date(); // Get the current date.
@@ -126,4 +126,48 @@ export const groupChatList = [
active: false,
last_seen: '1m',
},
+ {
+ user_id: 8,
+ name: 'GEN AI',
+ last_msg: "Let's catch up soon!",
+ modified_date: dateFormatter(user_3[1].createdAt),
+ profile_pic:
+ 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ9EWJXakOBhOQvhl8k0GCRsakU9RxV2m-qiQ&usqp=CAU',
+ msg_read: true,
+ active: false,
+ last_seen: '1m',
+ },
+ {
+ user_id: 8,
+ name: 'GEN AI',
+ last_msg: "Let's catch up soon!",
+ modified_date: dateFormatter(user_3[1].createdAt),
+ profile_pic:
+ 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ9EWJXakOBhOQvhl8k0GCRsakU9RxV2m-qiQ&usqp=CAU',
+ msg_read: true,
+ active: false,
+ last_seen: '1m',
+ },
+ {
+ user_id: 8,
+ name: 'GEN AI',
+ last_msg: "Let's catch up soon!",
+ modified_date: dateFormatter(user_3[1].createdAt),
+ profile_pic:
+ 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ9EWJXakOBhOQvhl8k0GCRsakU9RxV2m-qiQ&usqp=CAU',
+ msg_read: true,
+ active: false,
+ last_seen: '1m',
+ },
+ {
+ user_id: 8,
+ name: 'GEN AI',
+ last_msg: "Let's catch up soon!",
+ modified_date: dateFormatter(user_3[1].createdAt),
+ profile_pic:
+ 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ9EWJXakOBhOQvhl8k0GCRsakU9RxV2m-qiQ&usqp=CAU',
+ msg_read: true,
+ active: false,
+ last_seen: '1m',
+ },
];
diff --git a/src/Functions/Chat/Chat.js b/src/Functions/Chat/Chat.js
index 3c447687..051c302a 100644
--- a/src/Functions/Chat/Chat.js
+++ b/src/Functions/Chat/Chat.js
@@ -1,12 +1,30 @@
-import {useCallback, useState} from 'react';
+import {useCallback, useEffect, useRef, useState} from 'react';
import DocumentPicker from 'react-native-document-picker';
import {GiftedChat, InputToolbar} from 'react-native-gifted-chat';
+import AudioRecorderPlayer, {
+ AVEncoderAudioQualityIOSType,
+ AVEncodingOption,
+ AudioEncoderAndroidType,
+ AudioSourceAndroidType,
+ OutputFormatAndroidType,
+} from 'react-native-audio-recorder-player';
+import {Dimensions, PanResponder, PermissionsAndroid} from 'react-native';
+import TrackPlayer, {
+ useProgress,
+ usePlaybackState,
+} from 'react-native-track-player';
+import _ from 'lodash';
const ChatLogic = navigation => {
const [messages, setMessages] = useState([]);
const [attachments, setAttachments] = useState([]);
const [fileVisible, setFileVisible] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [audioURL, setAudioURL] = useState('');
+ const [recordingActive, setRecordingActive] = useState(false);
+ const [time, setTime] = useState(null);
+ const [isRecordingPaused, setIsRecordingPaused] = useState(false);
+ const [playingAudio, setPlayingAudio] = useState(false);
// current time retrieving
const currentTime = new Date().toLocaleTimeString('en-US', {
@@ -24,8 +42,9 @@ const ChatLogic = navigation => {
}, []);
const onSend = useCallback(
- (messages = []) => {
+ async (messages = []) => {
const [messageToSend] = messages;
+ // console.log(messages[0], ' TYU');
if (attachments.length > 0) {
const newMessages = attachments.map((attachment, index) => ({
_id: messageToSend._id + index + 1,
@@ -39,6 +58,9 @@ const ChatLogic = navigation => {
file: {
url: attachment.type === 'file' ? attachment.path : '',
},
+ video: {
+ url: attachment.type === 'video' ? attachment.path : '',
+ },
}));
setMessages(previousMessages =>
@@ -47,6 +69,39 @@ const ChatLogic = navigation => {
// Clear selected files after sending
setAttachments([]);
+ // console.log(messages, ' MESS');
+ } else if (audioURL !== '') {
+ try {
+ // Stop recording
+ await StopRecording();
+
+ // Create a new audio message
+ const newMessage = {
+ _id: messageToSend._id,
+ text: messages[0].text,
+ createdAt: new Date(),
+ user: {
+ _id: 2,
+ avatar: '',
+ },
+ audio: {
+ url: audioURL,
+ duration: time,
+ },
+ };
+
+ // Update messages state
+ setMessages(previousMessages =>
+ GiftedChat.append(previousMessages, newMessage),
+ );
+
+ // Clear recording states
+ setTime('');
+ setRecordingActive(false);
+ setAudioURL('');
+ } catch (error) {
+ console.error('Error while stopping recording:', error);
+ }
} else {
// Send regular text message
setMessages(previousMessages =>
@@ -54,7 +109,7 @@ const ChatLogic = navigation => {
);
}
},
- [attachments],
+ [attachments, audioURL, time],
);
const pickDocument = async () => {
@@ -77,7 +132,9 @@ const ChatLogic = navigation => {
type:
fileUri.includes('.png') || fileUri.includes('.jpg')
? 'image'
- : 'file',
+ : fileUri.includes('pdf')
+ ? 'file'
+ : 'video',
},
]);
}
@@ -102,6 +159,183 @@ const ChatLogic = navigation => {
return {modifiedProps};
};
+ // voice message
+ const [currentPositionSec, setCurrentPositionSec] = useState(0);
+ const [currentDurationSec, setCurrentDurationSec] = useState(0);
+ const [currentAudioId, setCurrentAudioId] = useState(null);
+
+ const {position, duration} = useProgress(0);
+ const {state} = usePlaybackState();
+
+ const intervalIdRef = useRef(null);
+
+ const path = Platform.select({
+ ios: undefined,
+ android: undefined,
+ });
+
+ const audioRecorderPlayer = useRef(new AudioRecorderPlayer());
+
+ useEffect(() => {
+ const initAudioRecorder = async () => {
+ try {
+ await audioRecorderPlayer.current.setSubscriptionDuration(0.1); // optional. Default is 0.5
+ } catch (error) {
+ console.error('Error initializing AudioRecorderPlayer:', error);
+ }
+ };
+
+ initAudioRecorder();
+ }, []);
+
+ let startTime = null;
+ let updateInterval = 100;
+
+ const StartTimer = () => {
+ // Clear the interval if it's already running
+ clearInterval(intervalIdRef.current);
+
+ startTime = Date.now();
+ intervalIdRef.current = setInterval(() => {
+ const elapsedTimeInSeconds = (Date.now() - startTime) / 1000;
+ const minutes = Math.floor(elapsedTimeInSeconds / 60);
+ const seconds = Math.floor(elapsedTimeInSeconds % 60);
+ const formattedTime = `${minutes}:${seconds.toString().padStart(2, '0')}`;
+ setTime(formattedTime);
+ }, updateInterval);
+ };
+
+ const StopTimer = () => {
+ console.log(' Stopped');
+ clearInterval(intervalIdRef.current);
+ setTime('');
+ };
+
+ const StartRecording = async () => {
+ if (Platform.OS === 'android') {
+ try {
+ const grants = await PermissionsAndroid.requestMultiple([
+ PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
+ PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
+ ]);
+
+ console.log('write external storage', grants);
+
+ if (
+ grants['android.permission.WRITE_EXTERNAL_STORAGE'] ===
+ PermissionsAndroid.RESULTS.GRANTED &&
+ grants['android.permission.READ_EXTERNAL_STORAGE'] ===
+ PermissionsAndroid.RESULTS.GRANTED &&
+ grants['android.permission.RECORD_AUDIO'] ===
+ PermissionsAndroid.RESULTS.GRANTED
+ ) {
+ console.log('permissions granted');
+ } else {
+ console.log('All required permissions not granted');
+ return;
+ }
+ } catch (err) {
+ console.warn(err);
+ return;
+ }
+ }
+
+ const audioSet = {
+ AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
+ AudioSourceAndroid: AudioSourceAndroidType.MIC,
+ AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
+ AVNumberOfChannelsKeyIOS: 2,
+ AVFormatIDKeyIOS: AVEncodingOption.aac,
+ OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS,
+ };
+
+ console.log('audioSet', audioSet);
+
+ setRecordingActive(true);
+ StartTimer();
+ const uri = await audioRecorderPlayer.current.startRecorder(path, audioSet);
+
+ console.log(`uri: ${uri}`);
+ setAudioURL(uri);
+ };
+
+ const StopRecording = async () => {
+ if (recordingActive) {
+ try {
+ console.log('HH');
+ await audioRecorderPlayer.current.stopRecorder();
+ // Additional cleanup or operations after stopping the recorder
+ clearInterval(intervalIdRef.current);
+ } catch (error) {
+ console.error('Error stopping recorder:', error);
+ }
+ }
+ };
+
+ function DeleteRecording() {
+ StopRecording();
+ setRecordingActive(false);
+ setAudioURL('');
+ }
+
+ const toggleRecording = async () => {
+ try {
+ if (!recordingActive) {
+ // Start recording
+ await StartRecording();
+ } else if (isRecordingPaused) {
+ // Resume recording
+ await audioRecorderPlayer.current.resumeRecorder();
+ setIsRecordingPaused(false);
+ StartTimer();
+ } else {
+ // Pause recording
+ StopRecording();
+ setIsRecordingPaused(true);
+ StopTimer();
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const Format = seconds => {
+ let mins = (parseInt(seconds / 60) % 60).toString();
+ let secs = Math.trunc(seconds % 60)
+ .toString()
+ .padStart(2, '0');
+ return `${mins}:${secs}`;
+ };
+
+ const playAudio = message => {
+ TrackPlayer.add({
+ id: message._id,
+ url: message.audio.url,
+ title: message.text,
+ });
+ TrackPlayer.play();
+ setPlayingAudio(true);
+ setCurrentAudioId(message._id);
+ };
+
+ const TogglePlayback = message => {
+ if (state === 'playing') {
+ TrackPlayer.pause();
+ setPlayingAudio(false);
+ setCurrentAudioId(null);
+ } else {
+ playAudio(message);
+ }
+ };
+
+ const onSliderValueChange = useCallback(
+ _.debounce(value => {
+ TrackPlayer.seekTo(value);
+ }, 300), // Adjust the debounce delay as needed
+ [],
+ );
+
return {
messages,
setMessages,
@@ -118,6 +352,32 @@ const ChatLogic = navigation => {
renderInputToolbar,
renderInputToolbar,
currentTime,
+ audioURL,
+ setAudioURL,
+ recordingActive,
+ setRecordingActive,
+ time,
+ setTime,
+ StartRecording,
+ StopRecording,
+ DeleteRecording,
+ isRecordingPaused,
+ toggleRecording,
+ playingAudio,
+ setPlayingAudio,
+ Format,
+ position,
+ duration,
+ state,
+ currentAudioId,
+ setCurrentAudioId,
+ currentDurationSec,
+ setCurrentDurationSec,
+ currentPositionSec,
+ setCurrentPositionSec,
+ playAudio,
+ TogglePlayback,
+ onSliderValueChange,
};
};
diff --git a/src/Functions/Chat/Contacts.js b/src/Functions/Chat/Contacts.js
index cbbc3b25..d4285674 100644
--- a/src/Functions/Chat/Contacts.js
+++ b/src/Functions/Chat/Contacts.js
@@ -19,7 +19,7 @@ const ContactsLogic = () => {
setFilteredContacts(filteredMessages);
};
- const HandleGroupSelection = item => {
+ const handleGroupSelection = item => {
const updatedGroupContact = new Set(groupContact);
if (updatedGroupContact.has(item)) {
@@ -45,7 +45,7 @@ const ContactsLogic = () => {
selected,
setSelected,
handleSearch,
- HandleGroupSelection,
+ handleGroupSelection,
};
};
diff --git a/src/Navigations/StackNavigator.jsx b/src/Navigations/StackNavigator.jsx
index 1ba2132d..ed47419e 100644
--- a/src/Navigations/StackNavigator.jsx
+++ b/src/Navigations/StackNavigator.jsx
@@ -36,7 +36,7 @@ import NewEvent from '../Containers/EventCalendar/NewEvent';
import EventList from '../Containers/EventCalendar/EventList';
import BottomTabNavigator from './Tab/BottomTabNavigator';
import TopTabNavigator from './Tab/TopTabNavigator';
-import DrawerNavigator from './Drawer/DrawerNavigator';
+import ChatVideoPlayer from '../Components/Video/ChatVideoPlayer';
export const initialState = {
isAudioEnabled: true,
@@ -57,50 +57,46 @@ const StackNavigator = () => {
const {drawer} = useAppContext();
return (
- //
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- //
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};