我正在开发聊天应用程序。我想像 WhatsApp 一样添加表情符号键盘。我的设计有问题。如果我打开表情符号键盘,我的输入字段会隐藏在此处,但如果打开系统键盘,它可以正常工作,这是我的代码。
我需要像系统键盘和 WhatsApp GUI 一样显示此键盘。请给我任何建议,因为我是反应原生的新手。
import React, { useEffect, useState,useRef } from 'react';
import { View, Text, StyleSheet, ImageBackground, TextInput,
FlatList,
TouchableOpacity,
Modal,
Alert,
Platform ,
ScrollView,
Animated,Easing,
TouchableWithoutFeedback,
KeyboardAvoidingView,Keyboard ,Dimensions } from 'react-native';
import moment from 'moment';
import { useNavigation } from '@react-navigation/native';
import {EmojiKeyboard } from 'rn-emoji-keyboard';
Sound.setCategory('Playback');
const { height: screenHeight, width: screenWidth } = Dimensions.get('window');
const SingleChat: React.FC<NativeStackScreenProps<NavigationProps, 'SingleChat'>> = ({ route }) => {
const userData = useSelector((state: RootState) => state.user.userData);
const { receiverData } = route.params;
const [isConnected, setIsConnected] = useState<boolean>(false);
const [msg, setMsg] = useState<string>('');
const [disabled, setDisabled] = useState<boolean>(false);
const [allChat, setAllChat] = useState<ChatMessage[]>([]);
const [voiceRecordVisible, setVoiceRecordVisible] = useState<boolean>(true);
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [newMessageId, setNewMessageId] = useState<string | null>(null);
const isFocused = useIsFocused();
const [ReceipentID, setReceipentID] = useState<string>('');
const [isRecording, setIsRecording] = useState(false);
const [recordingPath, setRecordingPath] = useState('');
const [AttachmentMenuvisible, setAttachmentMenuvisible] = useState(false);
const [menuPosition, setMenuPosition] = useState(0);
const [keyboardOffset, setKeyboardOffset] = useState(0);
const [keyboardHeight, setkeyboardHeight] = useState(0);
const [RecentDownloadedMessageID,SetDownloadedMessageID]=useState<string>('');
const [showEmoji, setShowEmoji] = useState(false);
const [isOpen, setIsOpen] = React.useState<boolean>(false)
const footerPosition = useRef(new Animated.Value(0)).current;
const [selectedAttachment, setSelectedAttachment] = useState<{
uri: string;
type: string;
name?: string;
} | null>(null);
const navigation =useNavigation();
const dispatch = useDispatch();
const slideAnim = useRef(new Animated.Value(300)).current; // Initial position below the screen
const openMenu = () => {
console.log('HERE i AM shOWING MENU ')
setAttachmentMenuvisible(true);
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
easing: Easing.ease,
useNativeDriver: true,
}).start();
};
const closeMenu = () => {
Animated.timing(slideAnim, {
toValue: 200, // Slide back down out of view
duration: 300,
easing: Easing.ease,
useNativeDriver: true,
}).start(() => {
setAttachmentMenuvisible(false);
});
};
const loadMessagesLocally = (roomId: string) => {
console.log(roomId);
try
{
getMessages(roomId, (messages: ChatMessage[]) => {
const sortedMessages = messages.sort((a, b) => new Date(b.sendTime).getTime() - new Date(a.sendTime).getTime());
setAllChat(sortedMessages);
});
}catch(error)
{
console.error('An Error Occured While Fetching local Messages',error);
}
};
useEffect(() => {
const handleConnectivityChange = (state: NetInfoState) => {
setIsConnected(state.isConnected ?? false);
};
const unsubscribe = NetInfo.addEventListener(handleConnectivityChange);
NetInfo.fetch().then((state) => {
handleConnectivityChange(state);
});
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', (event) => {
const keyboardHeight = event.endCoordinates.height;
console.log('here is the main heigh',keyboardHeight)
setkeyboardHeight(keyboardHeight)
setKeyboardOffset(keyboardHeight*0.1);
console.log('here is the main Scree height',keyboardOffset)
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardOffset(0);
});
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
}, []);
useEffect(() => {
console.log('Single Chat Use Effect Getting invoked...1')
const loadAndCompareMessages = async () => {
try {
const localMessages = await new Promise<ChatMessage[]>((resolve, reject) => {
getMessages(receiverData.roomId, (messages: ChatMessage[]) => {
resolve(messages);
});
});
const LocalsortedMessages = localMessages.sort((a, b) => {
const dateA = new Date(a.sendTime).getTime();
const dateB = new Date(b.sendTime).getTime();
if (isNaN(dateA) || isNaN(dateB)) {
console.warn('Invalid date format detected:', a.sendTime, b.sendTime);
return 0;
}
return dateB - dateA ;
});
setAllChat(LocalsortedMessages);
if (isConnected) {
const messagesRef = ref(db, `/messages/${receiverData.roomId}`);
const snapshot = await get(messagesRef);
const firebaseMessages: ChatMessage[] = [];
snapshot.forEach((childSnapshot) => {
const message = childSnapshot.val() as ChatMessage;
firebaseMessages.push(message);
});
const sortedFirebaseMessages = firebaseMessages.sort((a, b) => new Date(a.sendTime).getTime() - new Date(b.sendTime).getTime());
const localMessageIds = new Set(localMessages.map(msg => msg.localmsgid));
const validFirebaseMessages = sortedFirebaseMessages.filter(msg => msg.status !== 'uploading');
const newMessages = validFirebaseMessages.filter(msg => !localMessageIds.has(msg.localmsgid));
if (newMessages.length > 0) {
await saveMessages(newMessages);
}
const allMessages = [...localMessages, ...newMessages].sort((a, b) => new Date(b.sendTime).getTime() - new Date(a.sendTime).getTime());
setAllChat(allMessages);
}
} catch (error) {
console.error('An error occurred while loading and comparing messages:', error);
}
};
loadAndCompareMessages();
const messagesRef = ref(db, `/messages/${receiverData.roomId}`);
const unsubscribe = onValue(messagesRef, loadAndCompareMessages);
return () => unsubscribe();
}, [isConnected, userData.uid, receiverData.roomId,RecentDownloadedMessageID]);
const [recordingAnimation] = useState(new Animated.Value(0));
const startAnimation = () => {
Animated.loop(
Animated.sequence([
Animated.timing(recordingAnimation, {
toValue: 1,
duration: 500,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(recordingAnimation, {
toValue: 0,
duration: 500,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
])
).start();
};
const addEmoji = (emoji: EmojiType) => {
setMsg((prev) => prev + emoji.emoji);
if (!showEmoji) {
Keyboard.dismiss();
}
};
const stopAnimation = () => {
recordingAnimation.stopAnimation();
recordingAnimation.setValue(0);
};
const startRecording = async () => {
try {
const FileNameRandNumber = uuid.v4();
const path = Platform.select({
ios: `file://${RNFS.DocumentDirectoryPath}/${FileNameRandNumber}.m4a`,
android: `${RNFS.ExternalDirectoryPath}/${FileNameRandNumber}.mp3`,
});
const microphonePermission = Platform.select({
ios: PERMISSIONS.IOS.MICROPHONE,
android: PERMISSIONS.ANDROID.RECORD_AUDIO,
});
if (!microphonePermission) {
Alert.alert('Error', 'Required permissions are not available for this platform.');
return;
}
const micGranted = await request(microphonePermission);
if (micGranted === RESULTS.GRANTED) {
if (isRecording) {
await stopRecording();
}
const directoryPath = `${RNFS.CachesDirectoryPath}`; // Adjust if necessary
const cleanFileName = `${FileNameRandNumber}`;
const recordingPath = `${directoryPath}/${cleanFileName}`;
const directoryExists = await RNFS.exists(directoryPath);
if (!directoryExists) {
await RNFS.mkdir(directoryPath);
}
setIsRecording(true);
startAnimation();
const uri = await audioRecorderPlayer.startRecorder(path);
setRecordingPath(uri);
} else {
Alert.alert('Permission Denied', 'Please allow microphone and storage access.');
}
} catch (error) {
console.error('Error starting recording:', {
message: error.message,
stack: error.stack,
name: error.name,
});
Alert.alert('Recording Error', 'An error occurred while starting the recording.');
}
};
const stopRecording = async () => {
try {
if (!isRecording) {
console.warn('Recording is not active. Cannot stop.');
return;
}
console.log('Stopping recording...');
const result = await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
setIsRecording(false);
stopAnimation();
if (result) {
console.log('Recording stopped successfully:', recordingPath);
await uploadAudioToFirebase(recordingPath); // Use the existing recording path
} else {
console.warn('Recording result is null or undefined.');
}
} catch (error) {
console.error('Error stopping recording:', error);
Alert.alert('Recording Error', 'An error occurred while stopping the recording.');
setIsRecording(false);
}
};
const toggleEmojiKeyboard = () => {
setShowEmoji((prev) => !prev);
if (showEmoji) {
// Hide emoji picker and move footer down
Animated.timing(footerPosition, {
toValue: 0,
duration: 300,
useNativeDriver: false
}).start();
} else {
Keyboard.dismiss();
// Show emoji picker and move footer up
Animated.timing(footerPosition, {
toValue: screenHeight * 0.4, // Move up by 40% of the screen height (emoji picker height)
duration: 300,
useNativeDriver: false
}).start();
}
};
const msgValid = (txt: string) => txt && txt.trim().length > 0;
const handleSendMsg = () => {
if (!msgValid(msg)) {
SimpleToast.show('Enter something....', 9000);
return;
}
setDisabled(true);
sendMessage(msg, receiverData, userData, setMsg, setDisabled,'','text','','',0);
};
const pickImage = () => {
launchImageLibrary({ mediaType: 'photo', quality: 1 }, async (response) => {
if (response.assets && response.assets.length > 0) {
const asset = response.assets[0];
if (asset.uri) {
const blob = await fetch(asset.uri).then(r => r.blob());
const fileName = asset.fileName || `${uuid.v4()}.jpg`;
const fileType = asset.type || 'image/jpeg';
const fileSize = blob.size;
navigation.navigate('AttachmentPreview', { attachmentUri: asset.uri, attachmentType: 'image',recipientName:receiverData.name,receiverData:receiverData,userData:userData,responcePicker:asset });
try {
closeMenu();
} catch (error) {
console.error('Error uploading image:', error);
closeMenu();
setDisabled(false);
}
}
}
});
};
const AcknowledgeDownloadFile=async (messageID:string,FileDownloaded:boolean)=>{
if(FileDownloaded)
{
SetDownloadedMessageID(messageID);
}
}
const dismissEmojiPicker = () => {
if (isOpen) {
setIsOpen(false);
setShowEmoji(false);
}
};
const pickDocument = async () => {
try {
const res = await DocumentPicker.pick({
type: [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'video/mp4',
'video/x-msvideo',
'video/quicktime'
],
});
if (res && res.length > 0) {
const { uri, name, type, size } = res[0];
const cleanFileName = name??"".replace(/[^a-zA-Z0-9_.]/g, '_');
const sentFolderPath = `${RNFS.ExternalDirectoryPath}/ChipChap Sent`;
// Create the directory if it doesn't exist
const folderExists = await RNFS.exists(sentFolderPath);
if (!folderExists) {
await RNFS.mkdir(sentFolderPath);
}
const targetPath = `${sentFolderPath}/${cleanFileName}`;
await RNFS.copyFile(uri, targetPath);
const localFileUri = 'file://' + targetPath;
const attachmentType = type?.includes('pdf') ? 'pdf' :
type?.includes('video') ? 'video' :
'document';
navigation.navigate('AttachmentPreview', { attachmentUri: localFileUri, attachmentType: attachmentType,recipientName:receiverData.name,receiverData:receiverData,userData:userData,responcePicker:res });
// if (type?.includes('video')) {
// sendMessage('', receiverData, userData, setMsg, setDisabled, 'video', 'video', type ?? '', name ?? '', size??0,uri,localFileUri);
// } else {
// sendMessage('', receiverData, userData, setMsg, setDisabled, 'document', 'document', type ?? '', name ?? '', size??0,uri,localFileUri);
// }
closeMenu();
}
} catch (err) {
if (DocumentPicker.isCancel(err)) {
closeMenu();
} else {
console.error('Document Picker Error:', err);
}
}
};
return (
<TouchableWithoutFeedback onPress={toggleEmojiKeyboard}>
<View
style={styles.container}
>
<ChatHeader data={receiverData} />
<ImageBackground
source={require('../assets/ChatsList.jpg')}
style={{ flex: 1 }}
>
<FlatList
style={{ flex: 1 }}
data={allChat}
showsVerticalScrollIndicator={false}
keyExtractor={(item) => item.localmsgid ?? Math.random().toString()}
inverted
renderItem={({ item }) => (
<MsgComponent
sender={item.messageFrom === userData.uid}
item={{
message: item.message,
sendTime: item.sendTime,
seen: item.IsSeen,
msgType: item.msgType,
id: item.id,
localURL: item.localURL??"",
localmsgid: item.localmsgid,
documentType: item.documentType,
documentName: item.documentName,
documentSize: item.documentSize,
SenderName:receiverData.name,
caption:item.caption
}}
isNew={item.id === newMessageId}
AcknowledgeDownloadFile={AcknowledgeDownloadFile}
/>
)}
/>
</ImageBackground>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<View style={styles.footer}>
<Ionicons
name="attach"
size={32}
color={colors.white}
onPress={openMenu}
/>
{AttachmentMenuvisible && (
<Animated.View style={[styles.menuContainer, { transform: [{ translateX: slideAnim }] }]}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.menuScroll}>
<TouchableOpacity style={styles.menuItem} onPress={() => console.log('Contacts')}>
<Ionicons name="person-outline" size={24} color={colors.black} />
<Text>Contacts</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.menuItem} onPress={() =>pickImage()}>
<Ionicons name="images-outline" size={24} color={colors.black} />
<Text>Gallery</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.menuItem} onPress={() => pickDocument()}>
<Ionicons name="attach-outline" size={24} color={colors.black} />
<Text>Attachments</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.menuItem} onPress={() => console.log('Location')}>
<Ionicons name="location-outline" size={24} color={colors.black} />
<Text>Location</Text>
</TouchableOpacity>
</ScrollView>
</Animated.View>
)}
<View style={styles.textInputWrapper}>
<ScrollView>
<TextInput
style={styles.input}
placeholder="type a message"
placeholderTextColor={colors.black}
multiline={true}
value={msg}
onChangeText={setMsg}
onFocus={() => {
setVoiceRecordVisible(false);
if (showEmoji) {
setShowEmoji(false);
Animated.timing(footerPosition, {
toValue: 0,
duration: 300,
useNativeDriver: false
}).start();
}
}}
onBlur={() => {
setVoiceRecordVisible(true);
}}
/>
</ScrollView>
</View>
<TouchableOpacity style={styles.iconContainer} onPress={toggleEmojiKeyboard}>
<Ionicons name={showEmoji ? "keypad-outline" : "happy-outline"} size={32} color={colors.white} />
</TouchableOpacity>
{msg.trim() !== '' ? (
<TouchableOpacity disabled={disabled} onPress={handleSendMsg} style={styles.iconContainer}>
<Ionicons
name="paper-plane-sharp"
size={32}
color={colors.white}
/>
</TouchableOpacity>
) : voiceRecordVisible && !isRecording && (
<TouchableOpacity style={styles.iconContainer} onPress={startRecording}>
<Ionicons
name="mic"
size={32}
color={colors.white}
/>
</TouchableOpacity>
)}
{isRecording && (
<TouchableOpacity style={styles.iconContainer} onPress={stopRecording}>
<Ionicons
name="stop"
size={32}
color={colors.white}
/>
</TouchableOpacity>
)}
{isRecording && (
<Animated.View
style={[
styles.recordingIndicator,
{
opacity: recordingAnimation,
transform: [
{
scale: recordingAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.2],
}),
},
],
},
]}
>
<Text style={styles.recordingText}>Recording...</Text>
</Animated.View>
)}
{showEmoji && (
<View style={styles.emojiContainer}>
<EmojiKeyboard onEmojiSelected={addEmoji} />
</View>
)}
</View>
</KeyboardAvoidingView>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}
>
<View style={styles.modalContainer}>
<TouchableOpacity onPress={() => { setModalVisible(false); pickImage(); }} style={styles.modalButton}>
<Ionicons name="image" size={24} color={colors.black} />
<Text style={styles.modalText}>Gallery</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => { setModalVisible(false); pickDocument(); }} style={styles.modalButton}>
<Ionicons name="document-text" size={24} color={colors.black} />
<Text style={styles.modalText}>Document</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setModalVisible(false)} style={styles.modalButton}>
<Ionicons name="close" size={24} color={colors.black} />
<Text style={styles.modalText}>Cancel</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
</TouchableWithoutFeedback>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
bgImage: {
flex: 1,
resizeMode: 'cover',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
},
textInput: {
flex: 1,
backgroundColor: '#fff',
borderRadius: 20,
paddingHorizontal: 10,
paddingVertical: 5,
},
iconContainer: {
padding: 10,
},
modalContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalButton: {
backgroundColor: colors.white,
padding: 20,
borderRadius: 10,
marginVertical: 5,
flexDirection: 'row',
alignItems: 'center',
width: '80%',
justifyContent: 'space-between',
},
modalText: {
fontSize: 18,
color: colors.black,
},
footer: {
backgroundColor: colors.theme,
elevation: 5,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 10,
paddingVertical: 5,
},
input: {
flex: 1,
backgroundColor: colors.white,
borderRadius: 20,
paddingHorizontal: 10,
marginHorizontal: 10,
color: colors.black,
},
textInputWrapper: {
flex: 1,
maxHeight: 120,
},
menuContainer: {
position: 'absolute',
bottom: 60,
backgroundColor: 'white',
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
menuScroll: {
flexDirection: 'row',
padding: 10,
},
menuItem: {
alignItems: 'center',
marginHorizontal: 10,
},
recordingIndicator: {
position: 'absolute',
left: '50%',
top: '50%',
transform: [{ translateX: -50 }, { translateY: -50 }],
backgroundColor: 'rgba(255,0,0,0.7)',
padding: 10,
borderRadius: 50,
justifyContent: 'center',
alignItems: 'center',
},
recordingText: {
color: colors.white,
fontWeight: 'bold',
},
emojiContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: screenHeight * 0.4,
backgroundColor: 'white',
},
iconButton: {
padding: 10,
},
});
export default SingleChat;
我尝试使用动画视图,但这并没有给我带来 Whatsapp GUI 的感觉,我希望自动将我的文本输入字段滑到表情符号键盘上方。 请让我知道如何避免这种情况,因为我希望我的表情符号出现在键盘输入的正下方。
尝试将代码包装到 KeyboardAviodingView
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'height' : 'padding'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -keyboardHeight}
style={{
flex: 1,
}}>{...your code}
</KeyboardAvoidingView>