|
@@ -0,0 +1,969 @@
|
|
|
+import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
+import {
|
|
|
+ View,
|
|
|
+ TouchableOpacity,
|
|
|
+ Image,
|
|
|
+ Modal,
|
|
|
+ StyleSheet,
|
|
|
+ Text,
|
|
|
+ FlatList,
|
|
|
+ Dimensions,
|
|
|
+ Alert,
|
|
|
+ ScrollView
|
|
|
+} from 'react-native';
|
|
|
+import {
|
|
|
+ GiftedChat,
|
|
|
+ Bubble,
|
|
|
+ InputToolbar,
|
|
|
+ Actions,
|
|
|
+ IMessage,
|
|
|
+ Send,
|
|
|
+ BubbleProps,
|
|
|
+ QuickRepliesProps,
|
|
|
+ Composer
|
|
|
+} from 'react-native-gifted-chat';
|
|
|
+import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
+import EmojiSelector from 'react-native-emoji-selector';
|
|
|
+import * as ImagePicker from 'expo-image-picker';
|
|
|
+import { useActionSheet } from '@expo/react-native-action-sheet';
|
|
|
+import { ActionSheetProvider } from '@expo/react-native-action-sheet';
|
|
|
+import {
|
|
|
+ GestureHandlerRootView,
|
|
|
+ LongPressGestureHandler,
|
|
|
+ Swipeable
|
|
|
+} from 'react-native-gesture-handler';
|
|
|
+import { Header, PageWrapper } from 'src/components';
|
|
|
+import { Colors } from 'src/theme';
|
|
|
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
|
|
+import { Video } from 'expo-av';
|
|
|
+import ChatMessageBox from '../Components/ChatMessageBox';
|
|
|
+import ReplyMessageBar from '../Components/ReplyMessageBar';
|
|
|
+import Animated, {
|
|
|
+ FadeIn,
|
|
|
+ FadeOut,
|
|
|
+ SlideInDown,
|
|
|
+ SlideOutDown,
|
|
|
+ useSharedValue,
|
|
|
+ withTiming
|
|
|
+} from 'react-native-reanimated';
|
|
|
+import { BlurView } from 'expo-blur';
|
|
|
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
+import Clipboard from '@react-native-clipboard/clipboard';
|
|
|
+import { trigger } from 'react-native-haptic-feedback';
|
|
|
+import ReactModal from 'react-native-modal';
|
|
|
+import { storage, StoreType } from 'src/storage';
|
|
|
+import { usePostGetChatWithQuery, usePostSendMessageMutation } from '@api/chat';
|
|
|
+import { Message } from '../types';
|
|
|
+import { API_HOST } from 'src/constants';
|
|
|
+
|
|
|
+const options = {
|
|
|
+ enableVibrateFallback: true,
|
|
|
+ ignoreAndroidSystemSettings: false
|
|
|
+};
|
|
|
+
|
|
|
+const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
|
|
|
+
|
|
|
+const ChatScreen = ({ route }: { route: any }) => {
|
|
|
+ const { id, name, avatar }: { id: number; name: string; avatar: string | null } = route.params;
|
|
|
+ const currentUserId = storage.get('uid', StoreType.STRING) as number;
|
|
|
+ const token = storage.get('token', StoreType.STRING) as string;
|
|
|
+ const insets = useSafeAreaInsets();
|
|
|
+ const [messages, setMessages] = useState<IMessage[]>([]);
|
|
|
+ const { showActionSheetWithOptions } = useActionSheet();
|
|
|
+ const navigation = useNavigation();
|
|
|
+ const { data: chatData, isFetching, refetch } = usePostGetChatWithQuery(token, id, -1, -1, true);
|
|
|
+ const { mutateAsync: sendMessage } = usePostSendMessageMutation();
|
|
|
+
|
|
|
+ const swipeableRowRef = useRef<Swipeable | null>(null);
|
|
|
+ const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
|
|
|
+ const [selectedMedia, setSelectedMedia] = useState(null);
|
|
|
+
|
|
|
+ const [replyMessage, setReplyMessage] = useState<IMessage | null>(null);
|
|
|
+
|
|
|
+ const [selectedMessage, setSelectedMessage] = useState<IMessage | null>(null);
|
|
|
+ const [showReactions, setShowReactions] = useState<number | null>(null);
|
|
|
+ const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false);
|
|
|
+ const [messagePosition, setMessagePosition] = useState<{
|
|
|
+ x: number;
|
|
|
+ y: number;
|
|
|
+ width: number;
|
|
|
+ height: number;
|
|
|
+ isMine: boolean;
|
|
|
+ } | null>(null);
|
|
|
+
|
|
|
+ const [isModalVisible, setIsModalVisible] = useState(false);
|
|
|
+
|
|
|
+ const messageRefs = useRef<{ [key: string]: any }>({});
|
|
|
+ const scrollY = useSharedValue(0);
|
|
|
+
|
|
|
+ const mapApiMessageToGiftedMessage = (message: Message): IMessage => {
|
|
|
+ return {
|
|
|
+ _id: message.id,
|
|
|
+ text: message.text,
|
|
|
+ createdAt: new Date(message.sent_datetime + 'Z'),
|
|
|
+ user: {
|
|
|
+ _id: message.sender,
|
|
|
+ name: message.sender === id ? name : 'Me'
|
|
|
+ },
|
|
|
+ replyMessage: message.reply_to_id !== -1 ? { text: message.reply_to.text } : null,
|
|
|
+ reactions: JSON.parse(message.reactions || '{}'),
|
|
|
+ attachment: message.attachement !== -1 ? message.attachement : null,
|
|
|
+ pending: message.status === 1,
|
|
|
+ sent: message.status === 2,
|
|
|
+ received: message.status === 3
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ useFocusEffect(
|
|
|
+ useCallback(() => {
|
|
|
+ if (chatData?.messages) {
|
|
|
+ const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
|
|
|
+ setMessages(mappedMessages);
|
|
|
+ }
|
|
|
+ }, [chatData])
|
|
|
+ );
|
|
|
+
|
|
|
+ const clearReplyMessage = () => setReplyMessage(null);
|
|
|
+
|
|
|
+ const handleLongPress = (message: IMessage, props) => {
|
|
|
+ const messageRef = messageRefs.current[message._id];
|
|
|
+
|
|
|
+ setSelectedMessage(props);
|
|
|
+ trigger('impactMedium', options);
|
|
|
+
|
|
|
+ const isMine = message.user._id === +currentUserId;
|
|
|
+
|
|
|
+ if (messageRef) {
|
|
|
+ messageRef.measureInWindow((x: number, y: number, width: number, height: number) => {
|
|
|
+ const screenHeight = Dimensions.get('window').height;
|
|
|
+ const spaceAbove = y - insets.top;
|
|
|
+ const spaceBelow = screenHeight - (y + height) - insets.bottom * 2;
|
|
|
+
|
|
|
+ let finalY = y;
|
|
|
+ scrollY.value = 0;
|
|
|
+
|
|
|
+ if (isNaN(y) || isNaN(height)) {
|
|
|
+ console.error("Invalid measurement values for 'y' or 'height'", { y, height });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (spaceBelow < 150) {
|
|
|
+ const extraShift = 150 - spaceBelow;
|
|
|
+ finalY -= extraShift;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (spaceAbove < 50) {
|
|
|
+ const extraShift = 50 - spaceAbove;
|
|
|
+ finalY += extraShift;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (spaceBelow < 150 || spaceAbove < 50) {
|
|
|
+ const targetY = screenHeight / 2 - height / 2;
|
|
|
+ scrollY.value = withTiming(finalY - finalY);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (height > Dimensions.get('window').height - 200) {
|
|
|
+ finalY = 100;
|
|
|
+ }
|
|
|
+
|
|
|
+ finalY = isNaN(finalY) ? 0 : finalY;
|
|
|
+
|
|
|
+ setMessagePosition({ x, y: finalY, width, height, isMine });
|
|
|
+ setIsModalVisible(true);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const openEmojiSelector = () => {
|
|
|
+ setEmojiSelectorVisible(true);
|
|
|
+ trigger('impactLight', options);
|
|
|
+ };
|
|
|
+
|
|
|
+ const closeEmojiSelector = () => {
|
|
|
+ setEmojiSelectorVisible(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleReactionPress = (emoji: string, messageId: number) => {
|
|
|
+ if (emoji === '+') {
|
|
|
+ openEmojiSelector();
|
|
|
+ } else {
|
|
|
+ // addReaction(messageId, emoji);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleOptionPress = (option: string) => {
|
|
|
+ switch (option) {
|
|
|
+ case 'reply':
|
|
|
+ Alert.alert(option);
|
|
|
+ break;
|
|
|
+ case 'copy':
|
|
|
+ Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
|
|
|
+ Alert.alert('copied');
|
|
|
+ break;
|
|
|
+ case 'delete':
|
|
|
+ setMessages((prevMessages) =>
|
|
|
+ prevMessages.filter((msg) => msg._id !== selectedMessage?.currentMessage?._id)
|
|
|
+ );
|
|
|
+ Alert.alert('deleted');
|
|
|
+ break;
|
|
|
+ case 'pin':
|
|
|
+ Alert.alert(option);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ closeEmojiSelector();
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderReactionsBar = () =>
|
|
|
+ selectedMessage &&
|
|
|
+ messagePosition && (
|
|
|
+ <Animated.View
|
|
|
+ entering={FadeIn}
|
|
|
+ exiting={FadeOut}
|
|
|
+ style={[
|
|
|
+ styles.reactionBar,
|
|
|
+ {
|
|
|
+ top: messagePosition.y - 50, // - reaction bar height
|
|
|
+ left: messagePosition.isMine
|
|
|
+ ? Dimensions.get('window').width - Dimensions.get('window').width * 0.75 - 8 // reaction bar width
|
|
|
+ : messagePosition.x // + padding
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ {reactionEmojis.map((emoji) => (
|
|
|
+ <TouchableOpacity
|
|
|
+ key={emoji}
|
|
|
+ onPress={() => handleReactionPress(emoji, selectedMessage?.currentMessage?._id)}
|
|
|
+ >
|
|
|
+ <Text style={styles.reactionEmoji}>{emoji}</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+ ))}
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={() => openEmojiSelector()}
|
|
|
+ style={{
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 15,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: Colors.BORDER_LIGHT,
|
|
|
+ width: 30,
|
|
|
+ height: 30
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </Animated.View>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderOptionsMenu = () =>
|
|
|
+ selectedMessage &&
|
|
|
+ messagePosition && (
|
|
|
+ <Animated.View
|
|
|
+ entering={FadeIn}
|
|
|
+ exiting={FadeOut}
|
|
|
+ style={[
|
|
|
+ styles.optionsMenu,
|
|
|
+ {
|
|
|
+ top: messagePosition.y + messagePosition.height + 10,
|
|
|
+ left: messagePosition.isMine
|
|
|
+ ? Dimensions.get('window').width - Dimensions.get('window').width * 0.75 - 8
|
|
|
+ : messagePosition.x
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('reply')}>
|
|
|
+ <Text style={styles.optionText}>Reply</Text>
|
|
|
+ <MaterialCommunityIcons name="reply" size={20} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('copy')}>
|
|
|
+ <Text style={styles.optionText}>Copy</Text>
|
|
|
+ <MaterialCommunityIcons name="content-copy" size={20} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('delete')}>
|
|
|
+ <Text style={styles.optionText}>Delete</Text>
|
|
|
+ <MaterialCommunityIcons name="delete" size={20} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('pin')}>
|
|
|
+ <Text style={styles.optionText}>Pin message</Text>
|
|
|
+ <MaterialCommunityIcons name="pin" size={20} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </Animated.View>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderSelectedMessage = () =>
|
|
|
+ selectedMessage && (
|
|
|
+ <Animated.View
|
|
|
+ style={{
|
|
|
+ maxHeight: '80%',
|
|
|
+ position: 'absolute',
|
|
|
+ top: messagePosition?.y,
|
|
|
+ left: messagePosition?.x
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ScrollView>
|
|
|
+ <Bubble
|
|
|
+ {...selectedMessage}
|
|
|
+ currentMessage={selectedMessage?.currentMessage}
|
|
|
+ wrapperStyle={{
|
|
|
+ right: { backgroundColor: Colors.DARK_BLUE },
|
|
|
+ left: { backgroundColor: Colors.FILL_LIGHT }
|
|
|
+ }}
|
|
|
+ textStyle={{
|
|
|
+ right: { color: Colors.FILL_LIGHT },
|
|
|
+ left: { color: Colors.DARK_BLUE }
|
|
|
+ }}
|
|
|
+ renderTicks={(message: IMessage) => {
|
|
|
+ return message.user._id === +currentUserId && message.received ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
|
|
|
+ </View>
|
|
|
+ ) : message.user._id === +currentUserId && message.sent ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
|
|
|
+ </View>
|
|
|
+ ) : message.user._id === +currentUserId && message.pending ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
|
|
|
+ </View>
|
|
|
+ ) : null;
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </ScrollView>
|
|
|
+ </Animated.View>
|
|
|
+ );
|
|
|
+
|
|
|
+ const handleBackgroundPress = () => {
|
|
|
+ setIsModalVisible(false);
|
|
|
+ setSelectedMessage(null);
|
|
|
+ closeEmojiSelector();
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ navigation?.getParent()?.setOptions({
|
|
|
+ tabBarStyle: {
|
|
|
+ display: 'none'
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }, [navigation]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const intervalId = setInterval(() => {
|
|
|
+ refetch();
|
|
|
+ }, 5000);
|
|
|
+
|
|
|
+ return () => clearInterval(intervalId);
|
|
|
+ }, [refetch]);
|
|
|
+
|
|
|
+ const onSend = useCallback(
|
|
|
+ (newMessages: IMessage[] = []) => {
|
|
|
+ if (replyMessage) {
|
|
|
+ newMessages[0].replyMessage = {
|
|
|
+ text: replyMessage.text
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const message = { ...newMessages[0], pending: true };
|
|
|
+
|
|
|
+ sendMessage(
|
|
|
+ { to_uid: id, text: message.text },
|
|
|
+ {
|
|
|
+ onSuccess: (res) => console.log('res', res),
|
|
|
+ onError: (err) => console.log('err', err)
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages, [message]));
|
|
|
+ clearReplyMessage();
|
|
|
+ },
|
|
|
+ [replyMessage]
|
|
|
+ );
|
|
|
+
|
|
|
+ const openActionSheet = () => {
|
|
|
+ const options = ['Open Camera', 'Select from gallery', 'Cancel'];
|
|
|
+ const cancelButtonIndex = 2;
|
|
|
+
|
|
|
+ showActionSheetWithOptions(
|
|
|
+ {
|
|
|
+ options,
|
|
|
+ cancelButtonIndex
|
|
|
+ },
|
|
|
+ async (buttonIndex) => {
|
|
|
+ if (buttonIndex === 0) {
|
|
|
+ openCamera();
|
|
|
+ } else if (buttonIndex === 1) {
|
|
|
+ openGallery();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const openCamera = async () => {
|
|
|
+ const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
|
|
|
+
|
|
|
+ if (permissionResult.granted === false) {
|
|
|
+ alert('Permission denied to access camera');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await ImagePicker.launchCameraAsync({
|
|
|
+ mediaTypes: ImagePicker.MediaTypeOptions.All,
|
|
|
+ quality: 1,
|
|
|
+ allowsEditing: true
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!result.canceled) {
|
|
|
+ const newMedia = {
|
|
|
+ _id: Date.now().toString(),
|
|
|
+ createdAt: new Date(),
|
|
|
+ user: { _id: +currentUserId, name: 'Me' },
|
|
|
+ image: result.assets[0].type === 'image' ? result.assets[0].uri : null,
|
|
|
+ video: result.assets[0].type === 'video' ? result.assets[0].uri : null
|
|
|
+ };
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages, [newMedia as any]));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const openGallery = async () => {
|
|
|
+ const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
|
+
|
|
|
+ if (permissionResult.granted === false) {
|
|
|
+ alert('Denied');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await ImagePicker.launchImageLibraryAsync({
|
|
|
+ mediaTypes: ImagePicker.MediaTypeOptions.All,
|
|
|
+ allowsMultipleSelection: true,
|
|
|
+ quality: 1
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!result.canceled && result.assets) {
|
|
|
+ const imageMessages = result.assets.map((asset) => ({
|
|
|
+ _id: Date.now().toString() + asset.uri,
|
|
|
+ createdAt: new Date(),
|
|
|
+ user: { _id: +currentUserId, name: 'Me' },
|
|
|
+ image: asset.type === 'image' ? asset.uri : null,
|
|
|
+ video: asset.type === 'video' ? asset.uri : null
|
|
|
+ }));
|
|
|
+
|
|
|
+ setMessages((previousMessages) =>
|
|
|
+ GiftedChat.append(previousMessages, imageMessages as IMessage[])
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderMessageVideo = (props: any) => {
|
|
|
+ const { currentMessage } = props;
|
|
|
+
|
|
|
+ if (currentMessage.video) {
|
|
|
+ return (
|
|
|
+ <LongPressGestureHandler onHandlerStateChange={(event) => handleLongPress(currentMessage)}>
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={() => setSelectedMedia(currentMessage.video)}
|
|
|
+ style={styles.mediaContainer}
|
|
|
+ >
|
|
|
+ <Video
|
|
|
+ source={{ uri: currentMessage.video }}
|
|
|
+ style={styles.chatMedia}
|
|
|
+ useNativeControls
|
|
|
+ />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </LongPressGestureHandler>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ };
|
|
|
+
|
|
|
+ const addReaction = (messageId: number, reaction: any) => {
|
|
|
+ const updatedMessages = messages.map((msg: any) => {
|
|
|
+ if (msg._id === messageId) {
|
|
|
+ return {
|
|
|
+ ...msg,
|
|
|
+ reactions: [...(msg.reactions ?? []), reaction]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return msg;
|
|
|
+ });
|
|
|
+ setMessages(updatedMessages);
|
|
|
+ setShowReactions(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ const updateRowRef = useCallback(
|
|
|
+ (ref: any) => {
|
|
|
+ if (
|
|
|
+ ref &&
|
|
|
+ replyMessage &&
|
|
|
+ ref.props.children.props.currentMessage?._id === replyMessage._id
|
|
|
+ ) {
|
|
|
+ swipeableRowRef.current = ref;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [replyMessage]
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderReplyMessageView = (props: BubbleProps<IMessage>) =>
|
|
|
+ props.currentMessage &&
|
|
|
+ props.currentMessage?.replyMessage && (
|
|
|
+ <View style={styles.replyMessageContainer}>
|
|
|
+ <Text>{props.currentMessage.replyMessage.text}</Text>
|
|
|
+ <View style={styles.replyMessageDivider} />
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (replyMessage && swipeableRowRef.current) {
|
|
|
+ swipeableRowRef.current.close();
|
|
|
+ swipeableRowRef.current = null;
|
|
|
+ }
|
|
|
+ }, [replyMessage]);
|
|
|
+
|
|
|
+ const renderMessageImage = (props: any) => {
|
|
|
+ const { currentMessage } = props;
|
|
|
+ return (
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={() => setSelectedMedia(currentMessage.image)}
|
|
|
+ style={styles.imageContainer}
|
|
|
+ >
|
|
|
+ <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
|
|
|
+ </TouchableOpacity>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderBubble = (props: any) => {
|
|
|
+ const { currentMessage } = props;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View
|
|
|
+ ref={(ref) => {
|
|
|
+ if (ref && currentMessage) {
|
|
|
+ messageRefs.current[currentMessage._id] = ref;
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ collapsable={false}
|
|
|
+ >
|
|
|
+ <Bubble
|
|
|
+ key={`${currentMessage._id}`}
|
|
|
+ {...props}
|
|
|
+ wrapperStyle={{
|
|
|
+ right: {
|
|
|
+ backgroundColor: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ left: {
|
|
|
+ backgroundColor: Colors.FILL_LIGHT
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ textStyle={{
|
|
|
+ left: {
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ },
|
|
|
+ right: {
|
|
|
+ color: Colors.FILL_LIGHT
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onLongPress={() => handleLongPress(currentMessage, props)}
|
|
|
+ renderTicks={(message: IMessage) => {
|
|
|
+ return message.user._id === +currentUserId && message.received ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
|
|
|
+ </View>
|
|
|
+ ) : message.user._id === +currentUserId && message.sent ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
|
|
|
+ </View>
|
|
|
+ ) : message.user._id === +currentUserId && message.pending ? (
|
|
|
+ <View style={{ paddingRight: 8, bottom: 2 }}>
|
|
|
+ <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
|
|
|
+ </View>
|
|
|
+ ) : null;
|
|
|
+ }}
|
|
|
+ renderQuickReplies={(quickReplies: QuickRepliesProps<IMessage>) => null}
|
|
|
+ // renderQuickReplies={(quickReplies: QuickRepliesProps<IMessage>) => (
|
|
|
+ // <View style={{height: 20, width: 20, backgroundColor: 'green', bottom: 0, right: 0}}>
|
|
|
+ // <Text>{currentMessage.quickReplies.values[0].title}</Text>
|
|
|
+ // </View>
|
|
|
+
|
|
|
+ // )}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderInputToolbar = (props: any) => (
|
|
|
+ <InputToolbar
|
|
|
+ {...props}
|
|
|
+ renderActions={() => (
|
|
|
+ <Actions
|
|
|
+ icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
|
|
|
+ // onPressActionButton={openActionSheet}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderEmojiSelector = () => (
|
|
|
+ <Animated.View
|
|
|
+ entering={SlideInDown}
|
|
|
+ exiting={SlideOutDown}
|
|
|
+ style={styles.emojiSelectorContainer}
|
|
|
+ >
|
|
|
+ <EmojiSelector
|
|
|
+ onEmojiSelected={(emoji) => {
|
|
|
+ addReaction(selectedMessage?._id, emoji);
|
|
|
+ closeEmojiSelector();
|
|
|
+ }}
|
|
|
+ showSearchBar={true}
|
|
|
+ columns={8}
|
|
|
+ />
|
|
|
+ <TouchableOpacity style={styles.closeModalButton} onPress={closeEmojiSelector}>
|
|
|
+ <MaterialCommunityIcons name="close" size={30} color={Colors.DARK_BLUE} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </Animated.View>
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!messages.length) return null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <PageWrapper style={{ marginLeft: 0, marginRight: 0 }}>
|
|
|
+ <View style={{ paddingHorizontal: '5%' }}>
|
|
|
+ <Header
|
|
|
+ label={name}
|
|
|
+ rightElement={
|
|
|
+ <Image
|
|
|
+ source={{ uri: API_HOST + avatar }}
|
|
|
+ style={{
|
|
|
+ width: 30,
|
|
|
+ height: 30,
|
|
|
+ borderRadius: 15,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: Colors.FILL_LIGHT
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <GestureHandlerRootView style={styles.container}>
|
|
|
+ <GiftedChat
|
|
|
+ messages={messages}
|
|
|
+ listViewProps={{
|
|
|
+ showsVerticalScrollIndicator: false,
|
|
|
+ initialNumToRender: 20
|
|
|
+ }}
|
|
|
+ onSend={(newMessages: IMessage[]) => onSend(newMessages)}
|
|
|
+ user={{ _id: +currentUserId, name: 'Me' }}
|
|
|
+ renderBubble={renderBubble}
|
|
|
+ renderMessageImage={renderMessageImage}
|
|
|
+ renderInputToolbar={renderInputToolbar}
|
|
|
+ messageContainerRef={messageContainerRef}
|
|
|
+ renderSend={(props) => (
|
|
|
+ <View
|
|
|
+ style={{
|
|
|
+ flexDirection: 'row',
|
|
|
+ height: 44,
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ gap: 14,
|
|
|
+ paddingHorizontal: 14
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {props.text?.trim() && (
|
|
|
+ <Send
|
|
|
+ {...props}
|
|
|
+ containerStyle={{
|
|
|
+ justifyContent: 'center'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <MaterialCommunityIcons name="send" size={28} color={Colors.DARK_BLUE} />
|
|
|
+ </Send>
|
|
|
+ )}
|
|
|
+ {!props.text?.trim() && (
|
|
|
+ <>
|
|
|
+ {/* <MaterialCommunityIcons
|
|
|
+ name="microphone-outline"
|
|
|
+ size={28}
|
|
|
+ color={Colors.DARK_BLUE}
|
|
|
+ />
|
|
|
+
|
|
|
+ <MaterialCommunityIcons
|
|
|
+ name="camera-outline"
|
|
|
+ size={28}
|
|
|
+ color={Colors.DARK_BLUE}
|
|
|
+ /> */}
|
|
|
+ <MaterialCommunityIcons name="send" size={28} color={Colors.LIGHT_GRAY} />
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ textInputProps={styles.composer}
|
|
|
+ placeholder=""
|
|
|
+ renderMessage={(props) => (
|
|
|
+ <ChatMessageBox
|
|
|
+ {...props}
|
|
|
+ updateRowRef={updateRowRef}
|
|
|
+ setReplyOnSwipeOpen={setReplyMessage}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ renderChatFooter={() => (
|
|
|
+ <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
|
|
|
+ )}
|
|
|
+ renderCustomView={renderReplyMessageView}
|
|
|
+ renderMessageVideo={renderMessageVideo}
|
|
|
+ renderAvatar={null}
|
|
|
+ maxComposerHeight={100}
|
|
|
+ renderComposer={(props) => <Composer {...props} />}
|
|
|
+ isCustomViewBottom={true}
|
|
|
+ keyboardShouldPersistTaps="handled"
|
|
|
+ // inverted={true}
|
|
|
+ // isTyping={true}
|
|
|
+ />
|
|
|
+
|
|
|
+ <Modal visible={!!selectedMedia} transparent={true}>
|
|
|
+ <View style={styles.modalContainer}>
|
|
|
+ {selectedMedia && selectedMedia?.includes('.mp4') ? (
|
|
|
+ <Video
|
|
|
+ source={{ uri: selectedMedia }}
|
|
|
+ style={styles.fullScreenMedia}
|
|
|
+ // resizeMode="cover"
|
|
|
+ useNativeControls
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
|
|
|
+ )}
|
|
|
+ <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
|
|
|
+ <MaterialCommunityIcons name="close" size={30} color="white" />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </View>
|
|
|
+ </Modal>
|
|
|
+
|
|
|
+ <ReactModal
|
|
|
+ isVisible={isModalVisible}
|
|
|
+ onBackdropPress={handleBackgroundPress}
|
|
|
+ style={styles.reactModalContainer}
|
|
|
+ animationIn="fadeIn"
|
|
|
+ animationOut="fadeOut"
|
|
|
+ useNativeDriver
|
|
|
+ backdropColor="transparent"
|
|
|
+ >
|
|
|
+ <BlurView
|
|
|
+ intensity={80}
|
|
|
+ style={styles.modalBackground}
|
|
|
+ experimentalBlurMethod="dimezisBlurView"
|
|
|
+ >
|
|
|
+ <TouchableOpacity
|
|
|
+ style={styles.modalBackground}
|
|
|
+ activeOpacity={1}
|
|
|
+ onPress={handleBackgroundPress}
|
|
|
+ >
|
|
|
+ {renderReactionsBar()}
|
|
|
+ {renderSelectedMessage()}
|
|
|
+ {renderOptionsMenu()}
|
|
|
+ {emojiSelectorVisible ? renderEmojiSelector() : null}
|
|
|
+ </TouchableOpacity>
|
|
|
+ </BlurView>
|
|
|
+ </ReactModal>
|
|
|
+ </GestureHandlerRootView>
|
|
|
+ </PageWrapper>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ emojiSelectorContainer: {
|
|
|
+ position: 'absolute',
|
|
|
+ bottom: 0,
|
|
|
+ width: '100%',
|
|
|
+ height: '50%',
|
|
|
+ backgroundColor: 'white',
|
|
|
+ borderTopLeftRadius: 15,
|
|
|
+ borderTopRightRadius: 15,
|
|
|
+ shadowColor: '#000',
|
|
|
+ shadowOpacity: 0.3,
|
|
|
+ shadowOffset: { width: 0, height: -2 },
|
|
|
+ shadowRadius: 5,
|
|
|
+ elevation: 5,
|
|
|
+ padding: 10
|
|
|
+ },
|
|
|
+
|
|
|
+ modalBackground: {
|
|
|
+ flex: 1
|
|
|
+ },
|
|
|
+ modalContent: {
|
|
|
+ backgroundColor: 'transparent'
|
|
|
+ },
|
|
|
+ reactionBar: {
|
|
|
+ position: 'absolute',
|
|
|
+ width: Dimensions.get('window').width * 0.75,
|
|
|
+ flexDirection: 'row',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ alignItems: 'center',
|
|
|
+ borderRadius: 20,
|
|
|
+ padding: 5,
|
|
|
+ paddingHorizontal: 12,
|
|
|
+ shadowColor: '#000',
|
|
|
+ shadowOpacity: 0.3,
|
|
|
+ shadowOffset: { width: 0, height: 2 },
|
|
|
+ shadowRadius: 5,
|
|
|
+ elevation: 5
|
|
|
+ },
|
|
|
+ reactionEmoji: {
|
|
|
+ fontSize: 28
|
|
|
+ },
|
|
|
+ closeModalButton: {
|
|
|
+ position: 'absolute',
|
|
|
+ top: 10,
|
|
|
+ right: 10
|
|
|
+ },
|
|
|
+ optionsMenu: {
|
|
|
+ position: 'absolute',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
+ borderRadius: 10,
|
|
|
+ padding: 8,
|
|
|
+ shadowColor: '#000',
|
|
|
+ shadowOpacity: 0.3,
|
|
|
+ shadowOffset: { width: 0, height: 2 },
|
|
|
+ shadowRadius: 5,
|
|
|
+ elevation: 5,
|
|
|
+ width: Dimensions.get('window').width * 0.75
|
|
|
+ },
|
|
|
+ optionButton: {
|
|
|
+ paddingVertical: 10,
|
|
|
+ paddingHorizontal: 12,
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'space-between'
|
|
|
+ },
|
|
|
+ optionText: {
|
|
|
+ fontSize: 16
|
|
|
+ },
|
|
|
+ mediaContainer: {
|
|
|
+ borderRadius: 10,
|
|
|
+ overflow: 'hidden',
|
|
|
+ margin: 5
|
|
|
+ },
|
|
|
+ chatMedia: {
|
|
|
+ width: 200,
|
|
|
+ height: 200,
|
|
|
+ borderRadius: 10
|
|
|
+ },
|
|
|
+ fullScreenMedia: {
|
|
|
+ width: '90%',
|
|
|
+ height: '80%'
|
|
|
+ },
|
|
|
+ audioContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ padding: 10,
|
|
|
+ backgroundColor: '#f1f1f1',
|
|
|
+ borderRadius: 10,
|
|
|
+ margin: 5
|
|
|
+ },
|
|
|
+ progressBar: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'center',
|
|
|
+ alignItems: 'center'
|
|
|
+ },
|
|
|
+ bar: {
|
|
|
+ width: 5,
|
|
|
+ backgroundColor: 'gray',
|
|
|
+ marginHorizontal: 1
|
|
|
+ },
|
|
|
+ replyMessageContainer: {
|
|
|
+ padding: 8,
|
|
|
+ paddingBottom: 0
|
|
|
+ },
|
|
|
+ replyMessageDivider: {
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ borderBottomColor: 'lightgrey',
|
|
|
+ paddingTop: 6
|
|
|
+ },
|
|
|
+ composer: {
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
+ borderRadius: 15,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: Colors.LIGHT_GRAY,
|
|
|
+ paddingHorizontal: 10,
|
|
|
+ fontSize: 16,
|
|
|
+ marginVertical: 4
|
|
|
+ },
|
|
|
+ container: {
|
|
|
+ flex: 1,
|
|
|
+ backgroundColor: 'white'
|
|
|
+ },
|
|
|
+ imageContainer: {
|
|
|
+ borderRadius: 10,
|
|
|
+ overflow: 'hidden',
|
|
|
+ margin: 5
|
|
|
+ },
|
|
|
+ chatImage: {
|
|
|
+ width: 200,
|
|
|
+ height: 200,
|
|
|
+ borderRadius: 10
|
|
|
+ },
|
|
|
+ replyContainer: {
|
|
|
+ backgroundColor: '#f1f1f1',
|
|
|
+ padding: 5,
|
|
|
+ marginBottom: 5,
|
|
|
+ borderRadius: 5
|
|
|
+ },
|
|
|
+ replyText: {
|
|
|
+ color: '#333',
|
|
|
+ fontStyle: 'italic'
|
|
|
+ },
|
|
|
+ reactionsContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ justifyContent: 'flex-start',
|
|
|
+ paddingHorizontal: 5,
|
|
|
+ marginTop: -10
|
|
|
+ },
|
|
|
+ reactionText: {
|
|
|
+ fontSize: 16,
|
|
|
+ marginHorizontal: 5
|
|
|
+ },
|
|
|
+ reactionsBubble: {
|
|
|
+ backgroundColor: '#fff',
|
|
|
+ borderRadius: 20,
|
|
|
+ padding: 5,
|
|
|
+ flexDirection: 'row',
|
|
|
+ shadowColor: '#000',
|
|
|
+ shadowOpacity: 0.3,
|
|
|
+ shadowOffset: { width: 0, height: 2 },
|
|
|
+ shadowRadius: 5,
|
|
|
+ elevation: 5,
|
|
|
+ marginBottom: 10
|
|
|
+ },
|
|
|
+ replyInputContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ backgroundColor: '#f1f1f1',
|
|
|
+ paddingHorizontal: 10,
|
|
|
+ paddingVertical: 5,
|
|
|
+ borderRadius: 10
|
|
|
+ },
|
|
|
+ replyInputText: {
|
|
|
+ flex: 1,
|
|
|
+ color: 'gray'
|
|
|
+ },
|
|
|
+ modalContainer: {
|
|
|
+ flex: 1,
|
|
|
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
|
+ justifyContent: 'center',
|
|
|
+ alignItems: 'center'
|
|
|
+ },
|
|
|
+ reactModalContainer: {
|
|
|
+ justifyContent: 'flex-end',
|
|
|
+ margin: 0
|
|
|
+ },
|
|
|
+ fullScreenImage: {
|
|
|
+ width: '90%',
|
|
|
+ height: '80%'
|
|
|
+ },
|
|
|
+ closeButton: {
|
|
|
+ position: 'absolute',
|
|
|
+ top: 40,
|
|
|
+ right: 20
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+export default ChatScreen;
|