|
@@ -1,1658 +0,0 @@
|
|
|
-import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
-import {
|
|
|
- View,
|
|
|
- TouchableOpacity,
|
|
|
- Image,
|
|
|
- Modal,
|
|
|
- Text,
|
|
|
- FlatList,
|
|
|
- Dimensions,
|
|
|
- Alert,
|
|
|
- ScrollView,
|
|
|
- Linking,
|
|
|
- ActivityIndicator,
|
|
|
- AppState,
|
|
|
- AppStateStatus,
|
|
|
- TextInput,
|
|
|
- Keyboard,
|
|
|
- Platform,
|
|
|
- UIManager,
|
|
|
- LayoutAnimation,
|
|
|
- Animated,
|
|
|
- Easing,
|
|
|
- InputAccessoryView,
|
|
|
- KeyboardAvoidingView
|
|
|
-} from 'react-native';
|
|
|
-import {
|
|
|
- GiftedChat,
|
|
|
- Bubble,
|
|
|
- InputToolbar,
|
|
|
- IMessage,
|
|
|
- Send,
|
|
|
- BubbleProps,
|
|
|
- Composer,
|
|
|
- TimeProps,
|
|
|
- MessageProps,
|
|
|
- Actions
|
|
|
-} from 'react-native-gifted-chat';
|
|
|
-import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
-import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
|
|
|
-import { AvatarWithInitials, Header, WarningModal } from 'src/components';
|
|
|
-import { Colors } from 'src/theme';
|
|
|
-import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
|
|
-import { ResizeMode, Video } from 'expo-av';
|
|
|
-import ChatMessageBox from '../Components/ChatMessageBox';
|
|
|
-import ReplyMessageBar from '../Components/ReplyMessageBar';
|
|
|
-import { useSharedValue, withTiming } from 'react-native-reanimated';
|
|
|
-import { BlurView } from 'expo-blur';
|
|
|
-import { SafeAreaView, 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 {
|
|
|
- usePostDeleteMessageMutation,
|
|
|
- usePostGetChatWithQuery,
|
|
|
- usePostMessagesReadMutation,
|
|
|
- usePostReactToMessageMutation,
|
|
|
- usePostSendMessageMutation
|
|
|
-} from '@api/chat';
|
|
|
-import { CustomMessage, Message, Reaction } from '../types';
|
|
|
-import { API_HOST, WEBSOCKET_URL } from 'src/constants';
|
|
|
-import ReactionBar from '../Components/ReactionBar';
|
|
|
-import OptionsMenu from '../Components/OptionsMenu';
|
|
|
-import EmojiSelectorModal from '../Components/EmojiSelectorModal';
|
|
|
-import { styles } from './styles';
|
|
|
-import SendIcon from 'assets/icons/messages/send.svg';
|
|
|
-import { SheetManager } from 'react-native-actions-sheet';
|
|
|
-import { NAVIGATION_PAGES } from 'src/types';
|
|
|
-import { usePushNotification } from 'src/contexts/PushNotificationContext';
|
|
|
-import ReactionsListModal from '../Components/ReactionsListModal';
|
|
|
-import { dismissChatNotifications } from '../utils';
|
|
|
-import { useMessagesStore } from 'src/stores/unreadMessagesStore';
|
|
|
-import * as Location from 'expo-location';
|
|
|
-
|
|
|
-import BanIcon from 'assets/icons/messages/ban.svg';
|
|
|
-import AttachmentsModal from '../Components/AttachmentsModal';
|
|
|
-import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
|
|
|
-import ChatOptionsBlock from '../Components/ChatOptionsBlock';
|
|
|
-
|
|
|
-// if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
|
-// UIManager.setLayoutAnimationEnabledExperimental(false);
|
|
|
-// }
|
|
|
-
|
|
|
-const options = {
|
|
|
- enableVibrateFallback: true,
|
|
|
- ignoreAndroidSystemSettings: false
|
|
|
-};
|
|
|
-
|
|
|
-const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
|
|
|
-
|
|
|
-const ChatScreen = ({ route }: { route: any }) => {
|
|
|
- const token = storage.get('token', StoreType.STRING) as string;
|
|
|
- const {
|
|
|
- id,
|
|
|
- name,
|
|
|
- avatar,
|
|
|
- userType
|
|
|
- }: {
|
|
|
- id: number;
|
|
|
- name: string;
|
|
|
- avatar: string | null;
|
|
|
- userType: 'normal' | 'not_exist' | 'blocked';
|
|
|
- } = route.params;
|
|
|
- const userName =
|
|
|
- userType === 'blocked'
|
|
|
- ? 'Account is blocked'
|
|
|
- : userType === 'not_exist'
|
|
|
- ? 'Account does not exist'
|
|
|
- : name;
|
|
|
-
|
|
|
- const currentUserId = storage.get('uid', StoreType.STRING) as number;
|
|
|
- const insets = useSafeAreaInsets();
|
|
|
- const [messages, setMessages] = useState<CustomMessage[] | null>();
|
|
|
- const navigation = useNavigation();
|
|
|
- const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
|
|
|
- const {
|
|
|
- data: chatData,
|
|
|
- refetch,
|
|
|
- isFetching
|
|
|
- } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true);
|
|
|
- const { mutateAsync: sendMessage } = usePostSendMessageMutation();
|
|
|
-
|
|
|
- const swipeableRowRef = useRef<Swipeable | null>(null);
|
|
|
- const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
|
|
|
- const [selectedMedia, setSelectedMedia] = useState<any>(null);
|
|
|
-
|
|
|
- const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
|
|
|
- const [modalInfo, setModalInfo] = useState({
|
|
|
- visible: false,
|
|
|
- type: 'confirm',
|
|
|
- message: '',
|
|
|
- action: () => {},
|
|
|
- buttonTitle: '',
|
|
|
- title: ''
|
|
|
- });
|
|
|
-
|
|
|
- const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | 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 [unreadMessageIndex, setUnreadMessageIndex] = useState<number | null>(null);
|
|
|
- const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
|
|
|
- const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
|
|
|
- const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
|
|
|
-
|
|
|
- const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
|
|
|
- const [isRerendering, setIsRerendering] = useState<boolean>(false);
|
|
|
- const [isTyping, setIsTyping] = useState<boolean>(false);
|
|
|
-
|
|
|
- const messageRefs = useRef<{ [key: string]: any }>({});
|
|
|
- const flatList = useRef<FlatList | null>(null);
|
|
|
- const scrollY = useSharedValue(0);
|
|
|
- const { isSubscribed } = usePushNotification();
|
|
|
- const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
|
|
|
- const [hasMoreMessages, setHasMoreMessages] = useState(true);
|
|
|
- const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
|
|
|
-
|
|
|
- const [showOptions, setShowOptions] = useState<boolean>(false);
|
|
|
- const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
|
|
|
- const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
|
|
|
- const [lastKeyboardHeight, setLastKeyboardHeight] = useState(300);
|
|
|
-
|
|
|
- const appState = useRef(AppState.currentState);
|
|
|
- const textInputRef = useRef<TextInput>(null);
|
|
|
-
|
|
|
- const socket = useRef<WebSocket | null>(null);
|
|
|
-
|
|
|
- const closeModal = () => {
|
|
|
- setModalInfo({ ...modalInfo, visible: false });
|
|
|
- };
|
|
|
-
|
|
|
- const translateY = useRef(new Animated.Value(0)).current;
|
|
|
- const keyboardAnimDuration = useRef(250);
|
|
|
-
|
|
|
- // useEffect(() => {
|
|
|
- // const onKeyboardWillShow = (e: any) => {
|
|
|
- // setIsKeyboardOpen(true);
|
|
|
- // setLastKeyboardHeight(e.endCoordinates.height - insets.bottom || 300);
|
|
|
- // keyboardAnimDuration.current = e.duration;
|
|
|
- // };
|
|
|
-
|
|
|
- // const onKeyboardWillHide = (e: any) => {
|
|
|
- // setIsKeyboardOpen(false);
|
|
|
- // };
|
|
|
-
|
|
|
- // const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
|
- // const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
|
-
|
|
|
- // const kbShowSub = Keyboard.addListener(showEvent, onKeyboardWillShow);
|
|
|
- // const kbHideSub = Keyboard.addListener(hideEvent, onKeyboardWillHide);
|
|
|
-
|
|
|
- // return () => {
|
|
|
- // kbShowSub.remove();
|
|
|
- // kbHideSub.remove();
|
|
|
- // };
|
|
|
- // }, [insets.bottom, showOptions, translateY]);
|
|
|
-
|
|
|
- // useEffect(() => {
|
|
|
- // let toValue = 0;
|
|
|
- // if (isKeyboardOpen) {
|
|
|
- // toValue = keyboardHeight;
|
|
|
- // } else if (showOptions) {
|
|
|
- // toValue = keyboardHeight;
|
|
|
- // } else {
|
|
|
- // toValue = 0;
|
|
|
- // }
|
|
|
-
|
|
|
- // Animated.timing(translateY, {
|
|
|
- // toValue,
|
|
|
- // duration: keyboardAnimDuration.current ?? 250,
|
|
|
- // easing: Easing.inOut(Easing.ease),
|
|
|
- // useNativeDriver: false
|
|
|
- // }).start();
|
|
|
- // }, [isKeyboardOpen, showOptions, keyboardHeight]);
|
|
|
-
|
|
|
- const handleInputFocus = () => {
|
|
|
- // if (!showOptions) {
|
|
|
- // setImmediate(() => {
|
|
|
- // if (Platform.OS === 'ios') {
|
|
|
- // LayoutAnimation.configureNext({
|
|
|
- // duration: keyboardAnimDuration.current ?? 250,
|
|
|
- // update: { type: 'keyboard' }
|
|
|
- // });
|
|
|
- // }
|
|
|
- // setShowOptions(true);
|
|
|
- // Animated.timing(translateY, {
|
|
|
- // toValue: keyboardHeight,
|
|
|
- // duration: keyboardAnimDuration.current ?? 250,
|
|
|
- // easing: Easing.inOut(Easing.ease),
|
|
|
- // useNativeDriver: false
|
|
|
- // }).start();
|
|
|
- // });
|
|
|
- // }
|
|
|
- };
|
|
|
- const toggleOptions = useCallback(() => {
|
|
|
- if (showOptions && !isKeyboardOpen) {
|
|
|
- textInputRef.current?.focus();
|
|
|
- } else {
|
|
|
- if (isKeyboardOpen) {
|
|
|
- Keyboard.dismiss();
|
|
|
- setShowOptions(true);
|
|
|
- } else {
|
|
|
- if (Platform.OS === 'ios') {
|
|
|
- LayoutAnimation.configureNext({
|
|
|
- duration: keyboardAnimDuration.current ?? 250,
|
|
|
- update: { type: 'keyboard' }
|
|
|
- });
|
|
|
- }
|
|
|
- !showOptions && setShowOptions(true);
|
|
|
- }
|
|
|
- }
|
|
|
- }, [showOptions, isKeyboardOpen]);
|
|
|
-
|
|
|
- // const handleTouchOutside = () => {
|
|
|
- // if (showOptions) {
|
|
|
- // if (Platform.OS === 'ios') {
|
|
|
- // LayoutAnimation.configureNext({
|
|
|
- // duration: keyboardAnimDuration.current ?? 250,
|
|
|
- // update: { type: 'keyboard' }
|
|
|
- // });
|
|
|
- // }
|
|
|
- // setShowOptions(false);
|
|
|
- // }
|
|
|
- // Keyboard.dismiss();
|
|
|
- // };
|
|
|
-
|
|
|
- const onSendMedia = useCallback((files: { uri: string; type: 'image' }[]) => {
|
|
|
- const newMsgs = files.map((file) => {
|
|
|
- const msg: IMessage = {
|
|
|
- _id: Date.now() + Math.random(),
|
|
|
- text: '',
|
|
|
- createdAt: new Date(),
|
|
|
- user: { _id: +currentUserId, name: 'Me' }
|
|
|
- };
|
|
|
-
|
|
|
- if (file.type === 'image') {
|
|
|
- msg.image = file.uri;
|
|
|
- } else if (file.type === 'video') {
|
|
|
- msg.video = file.uri;
|
|
|
- }
|
|
|
- return msg;
|
|
|
- });
|
|
|
-
|
|
|
- setMessages((prev) => GiftedChat.append(prev, newMsgs));
|
|
|
- }, []);
|
|
|
-
|
|
|
- const onSendLocation = useCallback((coords: { latitude: number; longitude: number }) => {
|
|
|
- const locMsg: IMessage = {
|
|
|
- _id: Date.now() + Math.random(),
|
|
|
- text: `Location: lat=${coords.latitude}, lon=${coords.longitude}`,
|
|
|
- createdAt: new Date(),
|
|
|
- user: { _id: +currentUserId, name: 'Me' },
|
|
|
- location: coords
|
|
|
- };
|
|
|
- setMessages((prev) => GiftedChat.append(prev, [locMsg]));
|
|
|
- }, []);
|
|
|
-
|
|
|
- const onShareLiveLocation = useCallback(() => {
|
|
|
- const liveMsg: IMessage = {
|
|
|
- _id: 'live-loc-' + Date.now(),
|
|
|
- text: 'Sharing live location...',
|
|
|
- createdAt: new Date(),
|
|
|
- user: { _id: +currentUserId, name: 'Me' },
|
|
|
- system: false
|
|
|
- };
|
|
|
- setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
|
|
|
- }, []);
|
|
|
-
|
|
|
- const renderMessageVideo = (props: any) => {
|
|
|
- const { currentMessage } = props;
|
|
|
- if (!currentMessage?.video) return null;
|
|
|
-
|
|
|
- return (
|
|
|
- <View style={{ width: 200, height: 200, backgroundColor: '#000' }}>
|
|
|
- <Video
|
|
|
- source={{ uri: currentMessage.video }}
|
|
|
- style={{ flex: 1 }}
|
|
|
- useNativeControls
|
|
|
- resizeMode={ResizeMode.CONTAIN}
|
|
|
- />
|
|
|
- </View>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const renderOptionsView = () => {
|
|
|
- // if (!showOptions) return null;
|
|
|
- return (
|
|
|
- <Animated.View
|
|
|
- style={{
|
|
|
- transform: [{ translateY }]
|
|
|
- }}
|
|
|
- >
|
|
|
- <ChatOptionsBlock
|
|
|
- blockHeight={lastKeyboardHeight}
|
|
|
- closeOptions={() => setShowOptions(false)}
|
|
|
- onSendMedia={onSendMedia}
|
|
|
- onSendLocation={onSendLocation}
|
|
|
- onShareLiveLocation={onShareLiveLocation}
|
|
|
- />
|
|
|
- </Animated.View>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- let unsubscribe: any;
|
|
|
-
|
|
|
- const setupNotificationHandler = async () => {
|
|
|
- unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
|
|
|
- };
|
|
|
-
|
|
|
- setupNotificationHandler();
|
|
|
-
|
|
|
- return () => {
|
|
|
- if (unsubscribe) unsubscribe();
|
|
|
- updateUnreadMessagesCount();
|
|
|
- };
|
|
|
- }, [id]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- socket.current = new WebSocket(WEBSOCKET_URL);
|
|
|
-
|
|
|
- socket.current.onopen = () => {
|
|
|
- socket.current?.send(JSON.stringify({ token }));
|
|
|
- };
|
|
|
-
|
|
|
- socket.current.onmessage = (event) => {
|
|
|
- const data = JSON.parse(event.data);
|
|
|
- handleWebSocketMessage(data);
|
|
|
- };
|
|
|
-
|
|
|
- socket.current.onclose = () => {
|
|
|
- console.log('WebSocket connection closed chat screen');
|
|
|
- };
|
|
|
-
|
|
|
- return () => {
|
|
|
- if (socket.current) {
|
|
|
- socket.current.close();
|
|
|
- socket.current = null;
|
|
|
- }
|
|
|
- };
|
|
|
- }, [token]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- const handleAppStateChange = async (nextAppState: AppStateStatus) => {
|
|
|
- if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
|
- if (!socket.current || socket.current.readyState === WebSocket.CLOSED) {
|
|
|
- socket.current = new WebSocket(WEBSOCKET_URL);
|
|
|
- socket.current.onopen = () => {
|
|
|
- socket.current?.send(JSON.stringify({ token }));
|
|
|
- };
|
|
|
- socket.current.onmessage = (event) => {
|
|
|
- const data = JSON.parse(event.data);
|
|
|
- handleWebSocketMessage(data);
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
|
-
|
|
|
- return () => {
|
|
|
- subscription.remove();
|
|
|
- if (socket.current) {
|
|
|
- socket.current.close();
|
|
|
- socket.current = null;
|
|
|
- }
|
|
|
- };
|
|
|
- }, [token]);
|
|
|
-
|
|
|
- const handleWebSocketMessage = (data: any) => {
|
|
|
- switch (data.action) {
|
|
|
- case 'new_message':
|
|
|
- if (data.conversation_with === id && data.message) {
|
|
|
- const newMessage = mapApiMessageToGiftedMessage(data.message);
|
|
|
- setMessages((previousMessages) => {
|
|
|
- const messageExists =
|
|
|
- previousMessages && previousMessages.some((msg) => msg._id === newMessage._id);
|
|
|
- if (!messageExists) {
|
|
|
- return GiftedChat.append(previousMessages ?? [], [newMessage]);
|
|
|
- }
|
|
|
- return previousMessages;
|
|
|
- });
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'new_reaction':
|
|
|
- if (data.conversation_with === id && data.reaction) {
|
|
|
- updateMessageWithReaction(data.reaction);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'unreact':
|
|
|
- if (data.conversation_with === id && data.unreacted_message_id) {
|
|
|
- removeReactionFromMessage(data.unreacted_message_id);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'delete_message':
|
|
|
- if (data.conversation_with === id && data.deleted_message_id) {
|
|
|
- removeDeletedMessage(data.deleted_message_id);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'is_typing':
|
|
|
- if (data.conversation_with === id) {
|
|
|
- setIsTyping(true);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'stopped_typing':
|
|
|
- if (data.conversation_with === id) {
|
|
|
- setIsTyping(false);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case 'messages_read':
|
|
|
- if (data.conversation_with === id && data.read_messages_ids) {
|
|
|
- setMessages(
|
|
|
- (prevMessages) =>
|
|
|
- prevMessages?.map((msg) => {
|
|
|
- if (data.read_messages_ids.includes(msg._id)) {
|
|
|
- return { ...msg, received: true };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- }) ?? []
|
|
|
- );
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const updateMessageWithReaction = (reactionData: any) => {
|
|
|
- setMessages(
|
|
|
- (prevMessages) =>
|
|
|
- prevMessages?.map((msg) => {
|
|
|
- if (msg._id === reactionData.message_id) {
|
|
|
- const updatedReactions = [
|
|
|
- ...(Array.isArray(msg.reactions)
|
|
|
- ? msg.reactions?.filter((r: any) => r.uid !== reactionData.uid)
|
|
|
- : []),
|
|
|
- reactionData
|
|
|
- ];
|
|
|
- return { ...msg, reactions: updatedReactions };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- }) ?? []
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const removeReactionFromMessage = (messageId: number) => {
|
|
|
- setMessages(
|
|
|
- (prevMessages) =>
|
|
|
- prevMessages?.map((msg) => {
|
|
|
- if (msg._id === messageId) {
|
|
|
- const updatedReactions = Array.isArray(msg.reactions)
|
|
|
- ? msg.reactions?.filter((r: any) => r.uid !== id)
|
|
|
- : [];
|
|
|
- return { ...msg, reactions: updatedReactions };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- }) ?? []
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const removeDeletedMessage = (messageId: number) => {
|
|
|
- setMessages(
|
|
|
- (prevMessages) =>
|
|
|
- prevMessages?.map((msg) => {
|
|
|
- if (msg._id === messageId) {
|
|
|
- return {
|
|
|
- ...msg,
|
|
|
- deleted: true,
|
|
|
- text: 'This message was deleted',
|
|
|
- pending: false,
|
|
|
- sent: false,
|
|
|
- received: false
|
|
|
- };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- }) ?? []
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- const pingInterval = setInterval(() => {
|
|
|
- if (socket.current && socket.current.readyState === WebSocket.OPEN) {
|
|
|
- socket.current.send(JSON.stringify({ action: 'ping', conversation_with: id }));
|
|
|
- } else {
|
|
|
- socket.current = new WebSocket(WEBSOCKET_URL);
|
|
|
- socket.current.onopen = () => {
|
|
|
- socket.current?.send(JSON.stringify({ token }));
|
|
|
- };
|
|
|
- socket.current.onmessage = (event) => {
|
|
|
- const data = JSON.parse(event.data);
|
|
|
- handleWebSocketMessage(data);
|
|
|
- };
|
|
|
-
|
|
|
- return () => {
|
|
|
- if (socket.current) {
|
|
|
- socket.current.close();
|
|
|
- socket.current = null;
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
- }, 50000);
|
|
|
-
|
|
|
- return () => clearInterval(pingInterval);
|
|
|
- }, []);
|
|
|
-
|
|
|
- const sendWebSocketMessage = (
|
|
|
- action: string,
|
|
|
- message: CustomMessage | null = null,
|
|
|
- reaction: string | null = null,
|
|
|
- readMessagesIds: number[] | null = null
|
|
|
- ) => {
|
|
|
- if (socket.current && socket.current.readyState === WebSocket.OPEN) {
|
|
|
- const data: any = {
|
|
|
- action,
|
|
|
- conversation_with: id
|
|
|
- };
|
|
|
-
|
|
|
- if (action === 'new_message' && message) {
|
|
|
- data.message = {
|
|
|
- id: message._id,
|
|
|
- text: message.text,
|
|
|
- sender: +currentUserId,
|
|
|
- sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
|
|
- reply_to_id: message.replyMessage?.id ?? -1,
|
|
|
- reply_to: message.replyMessage ?? null,
|
|
|
- reactions: message.reactions ?? '{}',
|
|
|
- status: 2,
|
|
|
- attachement: -1
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (action === 'new_reaction' && message && reaction) {
|
|
|
- data.reaction = {
|
|
|
- message_id: message._id,
|
|
|
- reaction,
|
|
|
- uid: +currentUserId,
|
|
|
- datetime: new Date().toISOString()
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (action === 'unreact' && message) {
|
|
|
- data.message_id = message._id;
|
|
|
- }
|
|
|
-
|
|
|
- if (action === 'delete_message' && message) {
|
|
|
- data.message_id = message._id;
|
|
|
- }
|
|
|
-
|
|
|
- if (action === 'messages_read' && readMessagesIds) {
|
|
|
- data.messages_ids = readMessagesIds;
|
|
|
- }
|
|
|
-
|
|
|
- socket.current.send(JSON.stringify(data));
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const handleTyping = (isTyping: boolean) => {
|
|
|
- if (isTyping) {
|
|
|
- sendWebSocketMessage('is_typing');
|
|
|
- } else {
|
|
|
- sendWebSocketMessage('stopped_typing');
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
|
|
|
- return {
|
|
|
- _id: message.id,
|
|
|
- text: message.text,
|
|
|
- createdAt: new Date(message.sent_datetime + 'Z'),
|
|
|
- user: {
|
|
|
- _id: message.sender,
|
|
|
- name: message.sender === id ? userName : 'Me'
|
|
|
- },
|
|
|
- replyMessage:
|
|
|
- message.reply_to_id !== -1
|
|
|
- ? {
|
|
|
- text: message.reply_to.text,
|
|
|
- id: message.reply_to.id,
|
|
|
- name: message.reply_to.sender === id ? userName : 'Me'
|
|
|
- }
|
|
|
- : 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,
|
|
|
- deleted: message.status === 4
|
|
|
- };
|
|
|
- };
|
|
|
-
|
|
|
- useFocusEffect(
|
|
|
- useCallback(() => {
|
|
|
- refetch();
|
|
|
- }, [])
|
|
|
- );
|
|
|
-
|
|
|
- useFocusEffect(
|
|
|
- useCallback(() => {
|
|
|
- if (chatData?.messages) {
|
|
|
- const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
|
|
|
-
|
|
|
- if (unreadMessageIndex === null && !isFetching) {
|
|
|
- const firstUnreadIndex = mappedMessages.findLastIndex(
|
|
|
- (msg) => !msg.received && !msg?.deleted && msg.user._id === id
|
|
|
- );
|
|
|
-
|
|
|
- if (firstUnreadIndex !== -1) {
|
|
|
- setUnreadMessageIndex(firstUnreadIndex);
|
|
|
-
|
|
|
- const unreadMarker: any = {
|
|
|
- _id: 'unreadMarker',
|
|
|
- text: 'Unread messages',
|
|
|
- system: true
|
|
|
- };
|
|
|
-
|
|
|
- mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker);
|
|
|
- setTimeout(() => {
|
|
|
- if (flatList.current) {
|
|
|
- flatList.current.scrollToIndex({
|
|
|
- index: firstUnreadIndex,
|
|
|
- animated: true,
|
|
|
- viewPosition: 0.5
|
|
|
- });
|
|
|
- }
|
|
|
- }, 500);
|
|
|
- } else {
|
|
|
- setUnreadMessageIndex(0);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- setMessages((previousMessages) => {
|
|
|
- const newMessages = mappedMessages.filter(
|
|
|
- (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
|
|
|
- );
|
|
|
- return prevThenMessageId !== -1 && previousMessages
|
|
|
- ? GiftedChat.prepend(previousMessages, newMessages)
|
|
|
- : mappedMessages;
|
|
|
- });
|
|
|
-
|
|
|
- if (mappedMessages.length < 50) {
|
|
|
- setHasMoreMessages(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (mappedMessages.length === 0 && !modalInfo.visible) {
|
|
|
- setTimeout(() => {
|
|
|
- textInputRef.current?.focus();
|
|
|
- }, 500);
|
|
|
- }
|
|
|
-
|
|
|
- setIsLoadingEarlier(false);
|
|
|
- }
|
|
|
- }, [chatData])
|
|
|
- );
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (messages?.length === 0 && !modalInfo.visible) {
|
|
|
- setTimeout(() => {
|
|
|
- textInputRef.current?.focus();
|
|
|
- }, 500);
|
|
|
- }
|
|
|
- }, [modalInfo]);
|
|
|
-
|
|
|
- const loadEarlierMessages = async () => {
|
|
|
- if (!hasMoreMessages || isLoadingEarlier || !messages) return;
|
|
|
-
|
|
|
- setIsLoadingEarlier(true);
|
|
|
-
|
|
|
- const previousMessageId = messages[messages.length - 1]._id;
|
|
|
-
|
|
|
- setPrevThenMessageId(previousMessageId);
|
|
|
- };
|
|
|
-
|
|
|
- const sentToServer = useRef<Set<number>>(new Set());
|
|
|
-
|
|
|
- const handleViewableItemsChanged = ({ viewableItems }: { viewableItems: any[] }) => {
|
|
|
- const newViewableUnreadMessages = viewableItems
|
|
|
- .filter(
|
|
|
- (item) =>
|
|
|
- !item.item.received &&
|
|
|
- !item.item.deleted &&
|
|
|
- !item.item.system &&
|
|
|
- item.item.user._id === id &&
|
|
|
- !sentToServer.current.has(item.item._id)
|
|
|
- )
|
|
|
- .map((item) => item.item._id);
|
|
|
-
|
|
|
- if (newViewableUnreadMessages.length > 0) {
|
|
|
- markMessagesAsRead(
|
|
|
- {
|
|
|
- token,
|
|
|
- from_user: id,
|
|
|
- messages_id: newViewableUnreadMessages
|
|
|
- },
|
|
|
- {
|
|
|
- onSuccess: () => {
|
|
|
- newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
|
|
|
- sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages);
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const renderSystemMessage = (props: any) => {
|
|
|
- if (props.currentMessage._id === 'unreadMarker') {
|
|
|
- return (
|
|
|
- <View style={styles.unreadMessagesContainer}>
|
|
|
- <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
|
|
|
- </View>
|
|
|
- );
|
|
|
- }
|
|
|
- return null;
|
|
|
- };
|
|
|
-
|
|
|
- const clearReplyMessage = () => setReplyMessage(null);
|
|
|
-
|
|
|
- const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
|
|
|
- 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 < 160) {
|
|
|
- const extraShift = 160 - spaceBelow;
|
|
|
- finalY -= extraShift;
|
|
|
- }
|
|
|
-
|
|
|
- if (spaceAbove < 50) {
|
|
|
- const extraShift = 50 - spaceAbove;
|
|
|
- finalY += extraShift;
|
|
|
- }
|
|
|
-
|
|
|
- if (spaceBelow < 160 || 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 = () => {
|
|
|
- SheetManager.show('emoji-selector');
|
|
|
- trigger('impactLight', options);
|
|
|
- };
|
|
|
-
|
|
|
- const closeEmojiSelector = () => {
|
|
|
- SheetManager.hide('emoji-selector');
|
|
|
- };
|
|
|
-
|
|
|
- const handleReactionPress = (emoji: string, messageId: number) => {
|
|
|
- addReaction(messageId, emoji);
|
|
|
- };
|
|
|
-
|
|
|
- const handleDeleteMessage = (messageId: number) => {
|
|
|
- deleteMessage(
|
|
|
- {
|
|
|
- token,
|
|
|
- message_id: messageId,
|
|
|
- conversation_with_user: id
|
|
|
- },
|
|
|
- {
|
|
|
- onSuccess: () => {
|
|
|
- setMessages(
|
|
|
- (prevMessages) =>
|
|
|
- prevMessages?.map((msg) => {
|
|
|
- if (msg._id === messageId) {
|
|
|
- return {
|
|
|
- ...msg,
|
|
|
- deleted: true,
|
|
|
- text: 'This message was deleted',
|
|
|
- pending: false,
|
|
|
- sent: false,
|
|
|
- received: false
|
|
|
- };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- }) ?? []
|
|
|
- );
|
|
|
- const messageToDelete = messages?.find((msg) => msg._id === messageId);
|
|
|
- if (messageToDelete) {
|
|
|
- sendWebSocketMessage('delete_message', messageToDelete, null, null);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const handleOptionPress = (option: string) => {
|
|
|
- if (!selectedMessage) return;
|
|
|
-
|
|
|
- switch (option) {
|
|
|
- case 'reply':
|
|
|
- setReplyMessage(selectedMessage.currentMessage);
|
|
|
- setIsModalVisible(false);
|
|
|
- break;
|
|
|
- case 'copy':
|
|
|
- Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
|
|
|
- setIsModalVisible(false);
|
|
|
- Alert.alert('Copied');
|
|
|
- break;
|
|
|
- case 'delete':
|
|
|
- handleDeleteMessage(selectedMessage.currentMessage?._id);
|
|
|
- setIsModalVisible(false);
|
|
|
- break;
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- closeEmojiSelector();
|
|
|
- };
|
|
|
-
|
|
|
- const openReactionList = (
|
|
|
- reactions: { uid: number; name: string; reaction: string }[],
|
|
|
- messageId: number
|
|
|
- ) => {
|
|
|
- SheetManager.show('reactions-list-modal', {
|
|
|
- payload: {
|
|
|
- users: reactions,
|
|
|
- currentUserId: +currentUserId,
|
|
|
- token,
|
|
|
- messageId,
|
|
|
- conversation_with_user: id,
|
|
|
- setMessages,
|
|
|
- sendWebSocketMessage
|
|
|
- } as any
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const renderTimeContainer = (time: TimeProps<CustomMessage>) => {
|
|
|
- const createdAt = new Date(time.currentMessage.createdAt);
|
|
|
-
|
|
|
- const formattedTime = createdAt.toLocaleTimeString([], {
|
|
|
- hour: '2-digit',
|
|
|
- minute: '2-digit',
|
|
|
- hour12: true
|
|
|
- });
|
|
|
-
|
|
|
- const hasReactions =
|
|
|
- time.currentMessage.reactions &&
|
|
|
- Array.isArray(time.currentMessage.reactions) &&
|
|
|
- time.currentMessage.reactions.length > 0;
|
|
|
-
|
|
|
- return (
|
|
|
- <View
|
|
|
- style={[
|
|
|
- styles.bottomContainer,
|
|
|
- {
|
|
|
- justifyContent: hasReactions ? 'space-between' : 'flex-end'
|
|
|
- }
|
|
|
- ]}
|
|
|
- >
|
|
|
- {hasReactions && (
|
|
|
- <TouchableOpacity
|
|
|
- style={[
|
|
|
- styles.bottomCustomContainer,
|
|
|
- {
|
|
|
- backgroundColor:
|
|
|
- time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'
|
|
|
- }
|
|
|
- ]}
|
|
|
- onPress={() =>
|
|
|
- Array.isArray(time.currentMessage.reactions) &&
|
|
|
- openReactionList(
|
|
|
- time.currentMessage.reactions.map((reaction) => ({
|
|
|
- ...reaction,
|
|
|
- name: reaction.uid === id ? userName : 'Me'
|
|
|
- })),
|
|
|
- time.currentMessage._id
|
|
|
- )
|
|
|
- }
|
|
|
- >
|
|
|
- {Object.entries(
|
|
|
- (Array.isArray(time.currentMessage.reactions)
|
|
|
- ? time.currentMessage.reactions
|
|
|
- : []
|
|
|
- ).reduce(
|
|
|
- (acc: Record<string, { count: number }>, { reaction }: { reaction: string }) => {
|
|
|
- if (!acc[reaction]) {
|
|
|
- acc[reaction] = { count: 0 };
|
|
|
- }
|
|
|
- acc[reaction].count += 1;
|
|
|
- return acc;
|
|
|
- },
|
|
|
- {}
|
|
|
- )
|
|
|
- ).map(([emoji, { count }]: any) => {
|
|
|
- return (
|
|
|
- <View key={emoji}>
|
|
|
- <Text style={{}}>
|
|
|
- {emoji}
|
|
|
- {(count as number) > 1 ? ` ${count}` : ''}
|
|
|
- </Text>
|
|
|
- </View>
|
|
|
- );
|
|
|
- })}
|
|
|
- </TouchableOpacity>
|
|
|
- )}
|
|
|
- <View style={styles.timeContainer}>
|
|
|
- <Text style={styles.timeText}>{formattedTime}</Text>
|
|
|
- {renderTicks(time.currentMessage)}
|
|
|
- </View>
|
|
|
- </View>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const renderSelectedMessage = () =>
|
|
|
- selectedMessage && (
|
|
|
- <View
|
|
|
- style={{
|
|
|
- maxHeight: '80%',
|
|
|
- width: messagePosition?.width,
|
|
|
- position: 'absolute',
|
|
|
- top: messagePosition?.y,
|
|
|
- left: messagePosition?.x
|
|
|
- }}
|
|
|
- >
|
|
|
- <ScrollView>
|
|
|
- <Bubble
|
|
|
- {...selectedMessage}
|
|
|
- wrapperStyle={{
|
|
|
- right: { backgroundColor: Colors.DARK_BLUE },
|
|
|
- left: { backgroundColor: Colors.FILL_LIGHT }
|
|
|
- }}
|
|
|
- textStyle={{
|
|
|
- right: { color: Colors.WHITE },
|
|
|
- left: { color: Colors.DARK_BLUE }
|
|
|
- }}
|
|
|
- renderTicks={() => null}
|
|
|
- renderTime={renderTimeContainer}
|
|
|
- />
|
|
|
- </ScrollView>
|
|
|
- </View>
|
|
|
- );
|
|
|
-
|
|
|
- const handleBackgroundPress = () => {
|
|
|
- setIsModalVisible(false);
|
|
|
- setSelectedMessage(null);
|
|
|
- closeEmojiSelector();
|
|
|
- };
|
|
|
-
|
|
|
- useFocusEffect(
|
|
|
- useCallback(() => {
|
|
|
- navigation?.getParent()?.setOptions({
|
|
|
- tabBarStyle: {
|
|
|
- display: 'none'
|
|
|
- }
|
|
|
- });
|
|
|
- }, [navigation])
|
|
|
- );
|
|
|
-
|
|
|
- const onSend = useCallback(
|
|
|
- (newMessages: CustomMessage[] = []) => {
|
|
|
- if (replyMessage) {
|
|
|
- newMessages[0].replyMessage = {
|
|
|
- text: replyMessage.text,
|
|
|
- id: replyMessage._id,
|
|
|
- name: replyMessage.user._id === id ? userName : 'Me'
|
|
|
- };
|
|
|
- }
|
|
|
- const message = { ...newMessages[0], pending: true };
|
|
|
-
|
|
|
- setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
|
|
|
-
|
|
|
- sendMessage(
|
|
|
- {
|
|
|
- token,
|
|
|
- to_uid: id,
|
|
|
- text: message.text,
|
|
|
- reply_to_id: replyMessage ? (replyMessage._id as number) : -1
|
|
|
- },
|
|
|
- {
|
|
|
- onSuccess: (res) => {
|
|
|
- const newMessage = {
|
|
|
- _id: res.message_id,
|
|
|
- text: message.text,
|
|
|
- replyMessage: { ...message.replyMessage, sender: replyMessage?.user?._id }
|
|
|
- };
|
|
|
-
|
|
|
- setMessages((previousMessages) =>
|
|
|
- (previousMessages ?? []).map((msg) =>
|
|
|
- msg._id === message._id ? { ...msg, _id: res.message_id } : msg
|
|
|
- )
|
|
|
- );
|
|
|
- sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
|
|
|
- },
|
|
|
- onError: (err) => console.log('err', err)
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- clearReplyMessage();
|
|
|
- },
|
|
|
- [replyMessage]
|
|
|
- );
|
|
|
-
|
|
|
- const addReaction = (messageId: number, reaction: string) => {
|
|
|
- if (!messages) return;
|
|
|
-
|
|
|
- const updatedMessages = messages.map((msg: any) => {
|
|
|
- if (msg._id === messageId) {
|
|
|
- const updatedReactions: Reaction[] = [
|
|
|
- ...(Array.isArray(msg.reactions)
|
|
|
- ? msg.reactions?.filter((r: Reaction) => r.uid !== +currentUserId)
|
|
|
- : []),
|
|
|
- { datetime: new Date().toISOString(), reaction: reaction, uid: +currentUserId }
|
|
|
- ];
|
|
|
-
|
|
|
- return {
|
|
|
- ...msg,
|
|
|
- reactions: updatedReactions
|
|
|
- };
|
|
|
- }
|
|
|
- return msg;
|
|
|
- });
|
|
|
-
|
|
|
- setMessages(updatedMessages);
|
|
|
-
|
|
|
- reactToMessage(
|
|
|
- { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
|
|
|
- {
|
|
|
- onSuccess: () => {
|
|
|
- const message = messages.find((msg) => msg._id === messageId);
|
|
|
- if (message) {
|
|
|
- sendWebSocketMessage('new_reaction', message, reaction);
|
|
|
- }
|
|
|
- },
|
|
|
- onError: (err) => console.log('err', err)
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- setIsModalVisible(false);
|
|
|
- };
|
|
|
-
|
|
|
- const updateRowRef = useCallback(
|
|
|
- (ref: any) => {
|
|
|
- if (
|
|
|
- ref &&
|
|
|
- replyMessage &&
|
|
|
- ref.props.children.props.currentMessage?._id === replyMessage._id
|
|
|
- ) {
|
|
|
- swipeableRowRef.current = ref;
|
|
|
- }
|
|
|
- },
|
|
|
- [replyMessage]
|
|
|
- );
|
|
|
-
|
|
|
- const renderReplyMessageView = (props: BubbleProps<CustomMessage>) => {
|
|
|
- if (!props.currentMessage) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- const { currentMessage } = props;
|
|
|
-
|
|
|
- if (!currentMessage || !currentMessage?.replyMessage) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return (
|
|
|
- <TouchableOpacity
|
|
|
- style={[
|
|
|
- styles.replyMessageContainer,
|
|
|
- {
|
|
|
- backgroundColor:
|
|
|
- currentMessage.user._id === id ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.2)',
|
|
|
- borderColor: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE
|
|
|
- }
|
|
|
- ]}
|
|
|
- onPress={() => {
|
|
|
- if (currentMessage?.replyMessage?.id) {
|
|
|
- scrollToMessage(currentMessage.replyMessage.id);
|
|
|
- }
|
|
|
- }}
|
|
|
- >
|
|
|
- <View style={styles.replyContent}>
|
|
|
- <Text
|
|
|
- style={[
|
|
|
- styles.replyAuthorName,
|
|
|
- { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
|
|
|
- ]}
|
|
|
- >
|
|
|
- {currentMessage.replyMessage.name}
|
|
|
- </Text>
|
|
|
-
|
|
|
- <Text
|
|
|
- numberOfLines={1}
|
|
|
- style={[
|
|
|
- styles.replyMessageText,
|
|
|
- { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
|
|
|
- ]}
|
|
|
- >
|
|
|
- {currentMessage.replyMessage.text}
|
|
|
- </Text>
|
|
|
- </View>
|
|
|
- </TouchableOpacity>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const scrollToMessage = (messageId: number) => {
|
|
|
- if (!messages) return;
|
|
|
-
|
|
|
- const messageIndex = messages.findIndex((message) => message._id === messageId);
|
|
|
-
|
|
|
- if (messageIndex !== -1 && flatList.current) {
|
|
|
- flatList.current.scrollToIndex({
|
|
|
- index: messageIndex,
|
|
|
- animated: true,
|
|
|
- viewPosition: 0.5
|
|
|
- });
|
|
|
-
|
|
|
- setHighlightedMessageId(messageId);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (highlightedMessageId && isRerendering) {
|
|
|
- setTimeout(() => {
|
|
|
- setHighlightedMessageId(null);
|
|
|
- setIsRerendering(false);
|
|
|
- }, 1500);
|
|
|
- }
|
|
|
- }, [highlightedMessageId, isRerendering]);
|
|
|
-
|
|
|
- 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 renderTicks = (message: CustomMessage) => {
|
|
|
- if (message.user._id === id) return null;
|
|
|
-
|
|
|
- return message.received ? (
|
|
|
- <View>
|
|
|
- <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
|
|
|
- </View>
|
|
|
- ) : message.sent ? (
|
|
|
- <View>
|
|
|
- <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
|
|
|
- </View>
|
|
|
- ) : message.pending ? (
|
|
|
- <View>
|
|
|
- <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
|
|
|
- </View>
|
|
|
- ) : null;
|
|
|
- };
|
|
|
-
|
|
|
- const renderBubble = (props: BubbleProps<CustomMessage>) => {
|
|
|
- const { currentMessage } = props;
|
|
|
-
|
|
|
- if (currentMessage.deleted) {
|
|
|
- const text = currentMessage.text.length
|
|
|
- ? props.currentMessage.text
|
|
|
- : 'This message was deleted';
|
|
|
-
|
|
|
- return (
|
|
|
- <View>
|
|
|
- <Bubble
|
|
|
- {...props}
|
|
|
- renderTime={() => null}
|
|
|
- currentMessage={{
|
|
|
- ...props.currentMessage,
|
|
|
- text: text
|
|
|
- }}
|
|
|
- renderMessageText={() => (
|
|
|
- <View style={{ paddingHorizontal: 12, paddingVertical: 6 }}>
|
|
|
- <Text style={{ color: Colors.LIGHT_GRAY, fontStyle: 'italic', fontSize: 12 }}>
|
|
|
- {text}
|
|
|
- </Text>
|
|
|
- </View>
|
|
|
- )}
|
|
|
- wrapperStyle={{
|
|
|
- right: {
|
|
|
- backgroundColor: Colors.DARK_BLUE
|
|
|
- },
|
|
|
- left: {
|
|
|
- backgroundColor: Colors.FILL_LIGHT
|
|
|
- }
|
|
|
- }}
|
|
|
- textStyle={{
|
|
|
- left: {
|
|
|
- color: Colors.DARK_BLUE
|
|
|
- },
|
|
|
- right: {
|
|
|
- color: Colors.WHITE
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- </View>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- const isHighlighted = currentMessage._id === highlightedMessageId;
|
|
|
- const backgroundColor = isHighlighted
|
|
|
- ? Colors.ORANGE
|
|
|
- : currentMessage.user._id === +currentUserId
|
|
|
- ? Colors.DARK_BLUE
|
|
|
- : Colors.FILL_LIGHT;
|
|
|
-
|
|
|
- return (
|
|
|
- <View
|
|
|
- key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
|
|
|
- ref={(ref) => {
|
|
|
- if (ref && currentMessage) {
|
|
|
- messageRefs.current[currentMessage._id] = ref;
|
|
|
- }
|
|
|
- }}
|
|
|
- collapsable={false}
|
|
|
- >
|
|
|
- <Bubble
|
|
|
- {...props}
|
|
|
- wrapperStyle={{
|
|
|
- right: {
|
|
|
- backgroundColor: backgroundColor
|
|
|
- },
|
|
|
- left: {
|
|
|
- backgroundColor: backgroundColor
|
|
|
- }
|
|
|
- }}
|
|
|
- textStyle={{
|
|
|
- left: {
|
|
|
- color: Colors.DARK_BLUE
|
|
|
- },
|
|
|
- right: {
|
|
|
- color: Colors.FILL_LIGHT
|
|
|
- }
|
|
|
- }}
|
|
|
- onLongPress={() => handleLongPress(currentMessage, props)}
|
|
|
- renderTicks={() => null}
|
|
|
- renderTime={renderTimeContainer}
|
|
|
- />
|
|
|
- </View>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const openAttachmentsModal = () => {
|
|
|
- SheetManager.show('chat-attachments', {
|
|
|
- payload: {
|
|
|
- name: userName,
|
|
|
- uid: id,
|
|
|
- setModalInfo,
|
|
|
- closeOptions: () => setShowOptions(false),
|
|
|
- onSendMedia,
|
|
|
- onSendLocation,
|
|
|
- onShareLiveLocation
|
|
|
- } as any
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- const renderInputToolbar = (props: any) => (
|
|
|
- <View>
|
|
|
- <InputToolbar
|
|
|
- {...props}
|
|
|
- renderActions={() => (
|
|
|
- <Actions
|
|
|
- icon={() => (
|
|
|
- <MaterialCommunityIcons
|
|
|
- name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
|
|
|
- size={28}
|
|
|
- color={Colors.DARK_BLUE}
|
|
|
- />
|
|
|
- )}
|
|
|
- onPressActionButton={openAttachmentsModal}
|
|
|
- />
|
|
|
- )}
|
|
|
- containerStyle={{
|
|
|
- backgroundColor: Colors.FILL_LIGHT
|
|
|
- }}
|
|
|
- />
|
|
|
- {/* <InputAccessoryView nativeID={'inputAccessoryViewID'}>
|
|
|
- <InputToolbar
|
|
|
- {...props}
|
|
|
- renderActions={() => (
|
|
|
- <Actions
|
|
|
- icon={() => (
|
|
|
- <MaterialCommunityIcons
|
|
|
- name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
|
|
|
- size={28}
|
|
|
- color={Colors.DARK_BLUE}
|
|
|
- />
|
|
|
- )}
|
|
|
- onPressActionButton={toggleOptions}
|
|
|
- />
|
|
|
- )}
|
|
|
- containerStyle={{
|
|
|
- backgroundColor: Colors.FILL_LIGHT
|
|
|
- }}
|
|
|
- />
|
|
|
- </InputAccessoryView> */}
|
|
|
-
|
|
|
- {/* {showOptions && renderOptionsView()} */}
|
|
|
- </View>
|
|
|
- );
|
|
|
-
|
|
|
- const renderScrollToBottom = () => {
|
|
|
- return (
|
|
|
- <TouchableOpacity
|
|
|
- style={styles.scrollToBottom}
|
|
|
- onPress={() => {
|
|
|
- if (flatList.current) {
|
|
|
- flatList.current.scrollToIndex({ index: 0, animated: true });
|
|
|
- }
|
|
|
- }}
|
|
|
- >
|
|
|
- <MaterialCommunityIcons name="chevron-down" size={24} color={Colors.WHITE} />
|
|
|
- </TouchableOpacity>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const shouldUpdateMessage = (
|
|
|
- props: MessageProps<IMessage>,
|
|
|
- nextProps: MessageProps<IMessage>
|
|
|
- ) => {
|
|
|
- setIsRerendering(true);
|
|
|
- const currentId = nextProps.currentMessage._id;
|
|
|
- return currentId === highlightedMessageId;
|
|
|
- };
|
|
|
-
|
|
|
- return (
|
|
|
- <SafeAreaView
|
|
|
- edges={['top']}
|
|
|
- style={{
|
|
|
- height: '100%'
|
|
|
- }}
|
|
|
- >
|
|
|
- <KeyboardAvoidingView
|
|
|
- style={{ flex: 1 }}
|
|
|
- behavior={'height'}
|
|
|
- keyboardVerticalOffset={Platform.select({ android: 34 })}
|
|
|
- >
|
|
|
- <View style={{ paddingHorizontal: '5%' }}>
|
|
|
- <Header
|
|
|
- label={userName}
|
|
|
- textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
|
|
|
- rightElement={
|
|
|
- <TouchableOpacity
|
|
|
- onPress={() =>
|
|
|
- navigation.navigate(
|
|
|
- ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
|
|
|
- )
|
|
|
- }
|
|
|
- disabled={userType !== 'normal'}
|
|
|
- >
|
|
|
- {avatar && userType === 'normal' ? (
|
|
|
- <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
|
|
|
- ) : userType === 'normal' ? (
|
|
|
- <AvatarWithInitials
|
|
|
- text={
|
|
|
- name
|
|
|
- .split(/ (.+)/)
|
|
|
- .map((n) => n[0])
|
|
|
- .join('') ?? ''
|
|
|
- }
|
|
|
- flag={API_HOST + 'flag.png'}
|
|
|
- size={30}
|
|
|
- fontSize={12}
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <BanIcon fill={Colors.RED} width={30} height={30} />
|
|
|
- )}
|
|
|
- </TouchableOpacity>
|
|
|
- }
|
|
|
- />
|
|
|
- </View>
|
|
|
-
|
|
|
- <GestureHandlerRootView style={styles.container}>
|
|
|
- {messages ? (
|
|
|
- <GiftedChat
|
|
|
- messages={messages as CustomMessage[]}
|
|
|
- listViewProps={{
|
|
|
- ref: flatList,
|
|
|
- // onTouchStart: handleTouchOutside,
|
|
|
- // onTouchEnd: handleTouchOutside,
|
|
|
- showsVerticalScrollIndicator: false,
|
|
|
- initialNumToRender: 30,
|
|
|
- onViewableItemsChanged: handleViewableItemsChanged,
|
|
|
- viewabilityConfig: { itemVisiblePercentThreshold: 50 },
|
|
|
- onScrollToIndexFailed: (info: any) => {
|
|
|
- const wait = new Promise((resolve) => setTimeout(resolve, 300));
|
|
|
- wait.then(() => {
|
|
|
- flatList.current?.scrollToIndex({
|
|
|
- index: info.index,
|
|
|
- animated: true,
|
|
|
- viewPosition: 0.5
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- renderSystemMessage={renderSystemMessage}
|
|
|
- onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
|
|
|
- user={{ _id: +currentUserId, name: 'Me' }}
|
|
|
- renderBubble={renderBubble}
|
|
|
- renderMessageImage={renderMessageImage}
|
|
|
- renderInputToolbar={renderInputToolbar}
|
|
|
- renderCustomView={renderReplyMessageView}
|
|
|
- isCustomViewBottom={false}
|
|
|
- messageContainerRef={messageContainerRef}
|
|
|
- minComposerHeight={34}
|
|
|
- onInputTextChanged={(text) => handleTyping(text.length > 0)}
|
|
|
- textInputRef={textInputRef}
|
|
|
- isTyping={isTyping}
|
|
|
- renderSend={(props) => (
|
|
|
- <View style={styles.sendBtn}>
|
|
|
- {props.text?.trim() && (
|
|
|
- <Send
|
|
|
- {...props}
|
|
|
- containerStyle={{
|
|
|
- justifyContent: 'center'
|
|
|
- }}
|
|
|
- >
|
|
|
- <SendIcon fill={Colors.DARK_BLUE} />
|
|
|
- </Send>
|
|
|
- )}
|
|
|
- {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
|
|
|
- </View>
|
|
|
- )}
|
|
|
- renderMessageVideo={renderMessageVideo}
|
|
|
- textInputProps={{
|
|
|
- ...styles.composer,
|
|
|
- selectionColor: Colors.LIGHT_GRAY,
|
|
|
- onFocus: handleInputFocus
|
|
|
- }}
|
|
|
- isKeyboardInternallyHandled={false}
|
|
|
- placeholder=""
|
|
|
- renderMessage={(props) => (
|
|
|
- <ChatMessageBox
|
|
|
- {...(props as MessageProps<CustomMessage>)}
|
|
|
- updateRowRef={updateRowRef}
|
|
|
- setReplyOnSwipeOpen={setReplyMessage}
|
|
|
- />
|
|
|
- )}
|
|
|
- renderChatFooter={() => (
|
|
|
- <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
|
|
|
- )}
|
|
|
- renderAvatar={null}
|
|
|
- maxComposerHeight={100}
|
|
|
- renderComposer={(props) => <Composer {...props} />}
|
|
|
- keyboardShouldPersistTaps="handled"
|
|
|
- renderChatEmpty={() => (
|
|
|
- <View style={styles.emptyChat}>
|
|
|
- <Text
|
|
|
- style={styles.emptyChatText}
|
|
|
- >{`No messages yet.\nFeel free to start the conversation.`}</Text>
|
|
|
- </View>
|
|
|
- )}
|
|
|
- shouldUpdateMessage={shouldUpdateMessage}
|
|
|
- scrollToBottom={true}
|
|
|
- scrollToBottomComponent={renderScrollToBottom}
|
|
|
- scrollToBottomStyle={{ backgroundColor: 'transparent' }}
|
|
|
- parsePatterns={(linkStyle) => [
|
|
|
- {
|
|
|
- type: 'url',
|
|
|
- style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
|
|
|
- onPress: (url: string) => Linking.openURL(url),
|
|
|
- onLongPress: (url: string) => {
|
|
|
- Clipboard.setString(url ?? '');
|
|
|
- Alert.alert('Link copied');
|
|
|
- }
|
|
|
- }
|
|
|
- ]}
|
|
|
- infiniteScroll={true}
|
|
|
- loadEarlier={hasMoreMessages}
|
|
|
- isLoadingEarlier={isLoadingEarlier}
|
|
|
- onLoadEarlier={loadEarlierMessages}
|
|
|
- renderLoadEarlier={() => (
|
|
|
- <View style={{ paddingVertical: 20 }}>
|
|
|
- <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
|
|
|
- </View>
|
|
|
- )}
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
|
|
|
- )}
|
|
|
-
|
|
|
- <Modal visible={!!selectedMedia} transparent={true}>
|
|
|
- <View style={styles.modalContainer}>
|
|
|
- {selectedMedia && selectedMedia?.includes('.mp4') ? (
|
|
|
- <Video
|
|
|
- source={{ uri: selectedMedia }}
|
|
|
- style={styles.fullScreenMedia}
|
|
|
- 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}
|
|
|
- >
|
|
|
- <ReactionBar
|
|
|
- messagePosition={messagePosition}
|
|
|
- selectedMessage={selectedMessage}
|
|
|
- reactionEmojis={reactionEmojis}
|
|
|
- handleReactionPress={handleReactionPress}
|
|
|
- openEmojiSelector={openEmojiSelector}
|
|
|
- />
|
|
|
- {renderSelectedMessage()}
|
|
|
- <OptionsMenu
|
|
|
- selectedMessage={selectedMessage}
|
|
|
- handleOptionPress={handleOptionPress}
|
|
|
- messagePosition={messagePosition}
|
|
|
- />
|
|
|
- <EmojiSelectorModal
|
|
|
- visible={emojiSelectorVisible}
|
|
|
- selectedMessage={selectedMessage}
|
|
|
- addReaction={addReaction}
|
|
|
- closeEmojiSelector={closeEmojiSelector}
|
|
|
- />
|
|
|
- </TouchableOpacity>
|
|
|
- </BlurView>
|
|
|
- </ReactModal>
|
|
|
-
|
|
|
- <WarningModal
|
|
|
- isVisible={modalInfo.visible}
|
|
|
- onClose={closeModal}
|
|
|
- type={modalInfo.type}
|
|
|
- message={modalInfo.message}
|
|
|
- buttonTitle={modalInfo.buttonTitle}
|
|
|
- title={modalInfo.title}
|
|
|
- action={() => {
|
|
|
- modalInfo.action();
|
|
|
- closeModal();
|
|
|
- }}
|
|
|
- />
|
|
|
- <AttachmentsModal />
|
|
|
- <ReactionsListModal />
|
|
|
- <Animated.View
|
|
|
- style={{
|
|
|
- transform: [{ translateY }]
|
|
|
- }}
|
|
|
- >
|
|
|
- {/* {renderOptionsView()} */}
|
|
|
- </Animated.View>
|
|
|
- </GestureHandlerRootView>
|
|
|
- </KeyboardAvoidingView>
|
|
|
- {showOptions && renderOptionsView()}
|
|
|
- <View
|
|
|
- style={{
|
|
|
- height: insets.bottom,
|
|
|
- backgroundColor: Colors.FILL_LIGHT
|
|
|
- }}
|
|
|
- />
|
|
|
- </SafeAreaView>
|
|
|
- );
|
|
|
-};
|
|
|
-
|
|
|
-export default ChatScreen;
|