|
@@ -0,0 +1,1956 @@
|
|
|
|
+import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
|
+import {
|
|
|
|
+ View,
|
|
|
|
+ TouchableOpacity,
|
|
|
|
+ Image,
|
|
|
|
+ Text,
|
|
|
|
+ FlatList,
|
|
|
|
+ Dimensions,
|
|
|
|
+ Alert,
|
|
|
|
+ ScrollView,
|
|
|
|
+ Linking,
|
|
|
|
+ ActivityIndicator,
|
|
|
|
+ AppState,
|
|
|
|
+ AppStateStatus,
|
|
|
|
+ TextInput
|
|
|
|
+} from 'react-native';
|
|
|
|
+import {
|
|
|
|
+ GiftedChat,
|
|
|
|
+ Bubble,
|
|
|
|
+ InputToolbar,
|
|
|
|
+ IMessage,
|
|
|
|
+ Send,
|
|
|
|
+ BubbleProps,
|
|
|
|
+ Composer,
|
|
|
|
+ TimeProps,
|
|
|
|
+ MessageProps,
|
|
|
|
+ Actions,
|
|
|
|
+ isSameUser,
|
|
|
|
+ isSameDay
|
|
|
|
+} 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 { Audio } 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,
|
|
|
|
+ usePostGetGroupChatQuery,
|
|
|
|
+ usePostSendGroupMessageMutation,
|
|
|
|
+ usePostReactToGroupMessageMutation,
|
|
|
|
+ usePostGroupMessagesReadMutation
|
|
|
|
+} 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 '../ChatScreen/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 FileViewer from 'react-native-file-viewer';
|
|
|
|
+import * as FileSystem from 'expo-file-system';
|
|
|
|
+import ImageView from 'better-react-native-image-viewing';
|
|
|
|
+import * as MediaLibrary from 'expo-media-library';
|
|
|
|
+
|
|
|
|
+import BanIcon from 'assets/icons/messages/ban.svg';
|
|
|
|
+import AttachmentsModal from '../Components/AttachmentsModal';
|
|
|
|
+import RenderMessageVideo from '../Components/renderMessageVideo';
|
|
|
|
+import MessageLocation from '../Components/MessageLocation';
|
|
|
|
+import GroupIcon from 'assets/icons/messages/group-chat.svg';
|
|
|
|
+import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
|
|
|
|
+
|
|
|
|
+const options = {
|
|
|
|
+ enableVibrateFallback: true,
|
|
|
|
+ ignoreAndroidSystemSettings: false
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
|
|
|
|
+
|
|
|
|
+const GroupChatScreen = ({ route }: { route: any }) => {
|
|
|
|
+ const token = storage.get('token', StoreType.STRING) as string;
|
|
|
|
+ const {
|
|
|
|
+ group_token,
|
|
|
|
+ name,
|
|
|
|
+ avatar,
|
|
|
|
+ userType = 'normal'
|
|
|
|
+ }: {
|
|
|
|
+ group_token: string;
|
|
|
|
+ name: string;
|
|
|
|
+ avatar: string | null;
|
|
|
|
+ userType: 'normal' | 'not_exist' | 'blocked';
|
|
|
|
+ } = route.params;
|
|
|
|
+ const groupName =
|
|
|
|
+ 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 [groupAvatar, setGroupAvatar] = useState<string | null>(null);
|
|
|
|
+ const [messages, setMessages] = useState<CustomMessage[] | null>();
|
|
|
|
+ const navigation = useNavigation();
|
|
|
|
+ const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
|
|
|
|
+ const {
|
|
|
|
+ data: chatData,
|
|
|
|
+ refetch: refetch,
|
|
|
|
+ isFetching: isFetching
|
|
|
|
+ } = usePostGetGroupChatQuery(token, group_token, 50, prevThenMessageId, true);
|
|
|
|
+ const { mutateAsync: sendMessage } = usePostSendGroupMessageMutation();
|
|
|
|
+
|
|
|
|
+ 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 } = usePostGroupMessagesReadMutation();
|
|
|
|
+ const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
|
|
|
|
+ const { mutateAsync: reactToMessage } = usePostReactToGroupMessageMutation();
|
|
|
|
+
|
|
|
|
+ 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 appState = useRef(AppState.currentState);
|
|
|
|
+ const textInputRef = useRef<TextInput>(null);
|
|
|
|
+
|
|
|
|
+ const socket = useRef<WebSocket | null>(null);
|
|
|
|
+
|
|
|
|
+ const closeModal = () => {
|
|
|
|
+ setModalInfo({ ...modalInfo, visible: false });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ Audio.setAudioModeAsync({
|
|
|
|
+ allowsRecordingIOS: false,
|
|
|
|
+ staysActiveInBackground: false,
|
|
|
|
+ playsInSilentModeIOS: true,
|
|
|
|
+ shouldDuckAndroid: true,
|
|
|
|
+ playThroughEarpieceAndroid: false
|
|
|
|
+ });
|
|
|
|
+ }, []);
|
|
|
|
+
|
|
|
|
+ const onSendMedia = useCallback(
|
|
|
|
+ async (files: { uri: string; type: 'image' | 'video' }[]) => {
|
|
|
|
+ for (const file of files) {
|
|
|
|
+ const tempMessage: CustomMessage = {
|
|
|
|
+ _id: Date.now() + Math.random(),
|
|
|
|
+ text: '',
|
|
|
|
+ createdAt: new Date(),
|
|
|
|
+ user: { _id: +currentUserId, name: 'Me' },
|
|
|
|
+ reactions: {},
|
|
|
|
+ deleted: false,
|
|
|
|
+ attachment: {
|
|
|
|
+ id: -1,
|
|
|
|
+ filename: file.type,
|
|
|
|
+ filetype: file.type,
|
|
|
|
+ attachment_link: file.uri
|
|
|
|
+ },
|
|
|
|
+ pending: true,
|
|
|
|
+ isSending: true,
|
|
|
|
+ image: file.type === 'image' ? file.uri : undefined,
|
|
|
|
+ video: file.type === 'video' ? file.uri : undefined
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if (replyMessage) {
|
|
|
|
+ tempMessage.replyMessage = {
|
|
|
|
+ text: replyMessage.text,
|
|
|
|
+ id: replyMessage._id,
|
|
|
|
+ name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
|
|
|
|
+
|
|
|
|
+ const messageData = {
|
|
|
|
+ token,
|
|
|
|
+ to_group_token: group_token,
|
|
|
|
+ text: '',
|
|
|
|
+ reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
|
|
|
|
+ attachment: {
|
|
|
|
+ uri: file.uri,
|
|
|
|
+ type: file.type,
|
|
|
|
+ name: file.uri.split('/').pop()
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const res = await sendMessage(messageData, {
|
|
|
|
+ onSuccess: (res) => {
|
|
|
|
+ const { attachment, message_id } = res;
|
|
|
|
+
|
|
|
|
+ const newMessage = {
|
|
|
|
+ _id: message_id,
|
|
|
|
+ text: '',
|
|
|
|
+ attachment,
|
|
|
|
+ replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id },
|
|
|
|
+ image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
|
|
|
|
+ video: file.type === 'video' ? file.uri : undefined
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) =>
|
|
|
|
+ (previousMessages ?? []).map((msg) =>
|
|
|
|
+ msg._id === tempMessage._id
|
|
|
|
+ ? {
|
|
|
|
+ ...msg,
|
|
|
|
+ _id: res.message_id,
|
|
|
|
+ attachment: res.attachment,
|
|
|
|
+ isSending: false,
|
|
|
|
+ image:
|
|
|
|
+ res.attachment?.attachment_small_url && file.type === 'image'
|
|
|
|
+ ? API_HOST + res.attachment.attachment_small_url
|
|
|
|
+ : undefined,
|
|
|
|
+ video: res.attachment?.attachment_link
|
|
|
|
+ ? API_HOST + res.attachment.attachment_link
|
|
|
|
+ : undefined
|
|
|
|
+ }
|
|
|
|
+ : msg
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ clearReplyMessage();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ [replyMessage]
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const onSendLocation = useCallback(
|
|
|
|
+ async (coords: { latitude: number; longitude: number }) => {
|
|
|
|
+ const tempMessage: CustomMessage = {
|
|
|
|
+ _id: Date.now() + Math.random(),
|
|
|
|
+ text: '',
|
|
|
|
+ createdAt: new Date(),
|
|
|
|
+ user: { _id: +currentUserId, name: 'Me' },
|
|
|
|
+ pending: true,
|
|
|
|
+ deleted: false,
|
|
|
|
+ reactions: {},
|
|
|
|
+ attachment: {
|
|
|
|
+ id: -1,
|
|
|
|
+ filename: 'location.json',
|
|
|
|
+ filetype: 'nomadmania/location',
|
|
|
|
+ lat: coords.latitude,
|
|
|
|
+ lng: coords.longitude
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if (replyMessage) {
|
|
|
|
+ tempMessage.replyMessage = {
|
|
|
|
+ text: replyMessage.text,
|
|
|
|
+ id: replyMessage._id,
|
|
|
|
+ name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
|
|
|
|
+
|
|
|
|
+ const locationData = JSON.stringify({ lat: coords.latitude, lng: coords.longitude });
|
|
|
|
+ const fileUri = FileSystem.documentDirectory + 'location.json';
|
|
|
|
+ await FileSystem.writeAsStringAsync(fileUri, locationData);
|
|
|
|
+
|
|
|
|
+ const locationFile = {
|
|
|
|
+ uri: fileUri,
|
|
|
|
+ type: 'application/json',
|
|
|
|
+ name: 'location.json'
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const messageData = {
|
|
|
|
+ token,
|
|
|
|
+ to_group_token: group_token,
|
|
|
|
+ text: tempMessage.text,
|
|
|
|
+ reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
|
|
|
|
+ attachment: locationFile
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ sendMessage(messageData, {
|
|
|
|
+ onSuccess: async (res) => {
|
|
|
|
+ const { attachment, message_id } = res;
|
|
|
|
+
|
|
|
|
+ const newMessage = {
|
|
|
|
+ _id: message_id,
|
|
|
|
+ text: '',
|
|
|
|
+ attachment,
|
|
|
|
+ replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) =>
|
|
|
|
+ (previousMessages ?? []).map((msg) =>
|
|
|
|
+ msg._id === tempMessage._id ? { ...msg, _id: res.message_id } : msg
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
|
|
|
|
+ await FileSystem.deleteAsync(fileUri);
|
|
|
|
+ },
|
|
|
|
+ onError: async (err) => {
|
|
|
|
+ await FileSystem.deleteAsync(fileUri);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ clearReplyMessage();
|
|
|
|
+ },
|
|
|
|
+ [replyMessage]
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const onSendFile = useCallback(
|
|
|
|
+ (files: { uri: string; type: string; name?: string }[]) => {
|
|
|
|
+ const newMsgs = files.map((file) => {
|
|
|
|
+ const msg: CustomMessage = {
|
|
|
|
+ _id: Date.now() + Math.random(),
|
|
|
|
+ text: '',
|
|
|
|
+ createdAt: new Date(),
|
|
|
|
+ user: { _id: +currentUserId, name: 'Me' },
|
|
|
|
+ deleted: false,
|
|
|
|
+ reactions: {},
|
|
|
|
+ isSending: true,
|
|
|
|
+ attachment: {
|
|
|
|
+ id: -1,
|
|
|
|
+ filename: file.name ?? 'File',
|
|
|
|
+ filetype: file.type,
|
|
|
|
+ attachment_link: file.uri
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if (replyMessage) {
|
|
|
|
+ msg.replyMessage = {
|
|
|
|
+ text: replyMessage.text,
|
|
|
|
+ id: replyMessage._id,
|
|
|
|
+ name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (file.type.includes('image')) {
|
|
|
|
+ msg.image = file.uri;
|
|
|
|
+ } else if (file.type.includes('video')) {
|
|
|
|
+ msg.video = file.uri;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [msg]));
|
|
|
|
+
|
|
|
|
+ const messageData = {
|
|
|
|
+ token,
|
|
|
|
+ to_group_token: group_token,
|
|
|
|
+ text: '',
|
|
|
|
+ reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
|
|
|
|
+ attachment: {
|
|
|
|
+ uri: file.uri,
|
|
|
|
+ type: file.type,
|
|
|
|
+ name: file.name || file.uri.split('/').pop()
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ sendMessage(messageData, {
|
|
|
|
+ onSuccess: (res) => {
|
|
|
|
+ const { attachment, message_id } = res;
|
|
|
|
+
|
|
|
|
+ const newMessage = {
|
|
|
|
+ _id: message_id,
|
|
|
|
+ text: '',
|
|
|
|
+ attachment,
|
|
|
|
+ replyMessage: { ...msg.replyMessage, sender: replyMessage?.user?._id },
|
|
|
|
+ image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
|
|
|
|
+ video: file.type === 'video' ? file.uri : undefined
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) =>
|
|
|
|
+ (previousMessages ?? []).map((prevMsg) =>
|
|
|
|
+ prevMsg._id === msg._id
|
|
|
|
+ ? {
|
|
|
|
+ ...prevMsg,
|
|
|
|
+ _id: res.message_id,
|
|
|
|
+ attachment: res.attachment,
|
|
|
|
+ isSending: false,
|
|
|
|
+ image:
|
|
|
|
+ res.attachment?.attachment_small_url && file.type?.startsWith('image')
|
|
|
|
+ ? API_HOST + res.attachment.attachment_small_url
|
|
|
|
+ : undefined,
|
|
|
|
+ video:
|
|
|
|
+ res.attachment?.attachment_link && file.type?.startsWith('video')
|
|
|
|
+ ? API_HOST + res.attachment.attachment_link
|
|
|
|
+ : undefined
|
|
|
|
+ }
|
|
|
|
+ : prevMsg
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return msg;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ clearReplyMessage();
|
|
|
|
+ },
|
|
|
|
+ [replyMessage]
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ async function openFileInApp(uri: string, fileName: string) {
|
|
|
|
+ try {
|
|
|
|
+ const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
|
|
|
|
+ if (!dirExist.exists) {
|
|
|
|
+ await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
|
|
|
|
+
|
|
|
|
+ const fileExists = await FileSystem.getInfoAsync(fileUri);
|
|
|
|
+ if (fileExists.exists) {
|
|
|
|
+ await FileViewer.open(fileUri, {
|
|
|
|
+ showOpenWithDialog: true,
|
|
|
|
+ showAppsSuggestions: true
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
|
|
|
|
+ headers: { Nmtoken: token }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ await FileViewer.open(localUri, {
|
|
|
|
+ showOpenWithDialog: true,
|
|
|
|
+ showAppsSuggestions: true
|
|
|
|
+ });
|
|
|
|
+ } catch (err) {
|
|
|
|
+ console.warn('openFileInApp error:', err);
|
|
|
|
+ Alert.alert('Cannot open file', 'No application found to open this file.');
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ async function downloadFileToDevice(currentMessage: CustomMessage) {
|
|
|
|
+ if (!currentMessage.image && !currentMessage.video) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const fileUrl = currentMessage.video
|
|
|
|
+ ? currentMessage.video
|
|
|
|
+ : API_HOST + currentMessage.attachment?.attachment_full_url;
|
|
|
|
+ const fileType = currentMessage.attachment?.filetype || '';
|
|
|
|
+ const fileExt = fileType.split('/').pop() || '';
|
|
|
|
+ const fileName = currentMessage.attachment?.filename?.split('.')[0] || 'file';
|
|
|
|
+ const fileUri = `${FileSystem.cacheDirectory}${fileName}.${fileExt}`;
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
|
|
+ if (status !== 'granted') {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const downloadOptions = currentMessage.video ? { headers: { Nmtoken: token } } : undefined;
|
|
|
|
+ const { uri } = await FileSystem.downloadAsync(fileUrl, fileUri, downloadOptions);
|
|
|
|
+
|
|
|
|
+ await MediaLibrary.createAssetAsync(uri);
|
|
|
|
+
|
|
|
|
+ Alert.alert(
|
|
|
|
+ 'Success',
|
|
|
|
+ `${fileType.startsWith('video') ? 'Video' : 'Image'} saved to gallery.`
|
|
|
|
+ );
|
|
|
|
+ } catch (error) {
|
|
|
|
+ Alert.alert('Error', 'Failed to download the file.');
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ const renderMessageFile = (props: BubbleProps<CustomMessage>) => {
|
|
|
|
+ const { currentMessage } = props;
|
|
|
|
+ const leftMessage = currentMessage?.user?._id !== +currentUserId;
|
|
|
|
+ if (!currentMessage?.attachment) return null;
|
|
|
|
+
|
|
|
|
+ const { attachment_link, filename } = currentMessage.attachment;
|
|
|
|
+ const fileName = filename ?? 'Attachment';
|
|
|
|
+ const uri = API_HOST + attachment_link;
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <TouchableOpacity
|
|
|
|
+ style={[
|
|
|
|
+ styles.fileContainer,
|
|
|
|
+ { backgroundColor: leftMessage ? 'rgba(15, 63, 79, 0.2)' : 'rgba(244, 244, 244, 0.2)' }
|
|
|
|
+ ]}
|
|
|
|
+ onPress={() => {
|
|
|
|
+ openFileInApp(uri, fileName);
|
|
|
|
+ }}
|
|
|
|
+ onLongPress={() => handleLongPress(currentMessage, props)}
|
|
|
|
+ disabled={currentMessage?.isSending}
|
|
|
|
+ >
|
|
|
|
+ {currentMessage?.isSending ? (
|
|
|
|
+ <ActivityIndicator
|
|
|
|
+ size="small"
|
|
|
|
+ color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
|
|
|
|
+ />
|
|
|
|
+ ) : (
|
|
|
|
+ <MaterialCommunityIcons
|
|
|
|
+ name="file"
|
|
|
|
+ size={32}
|
|
|
|
+ color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ <Text
|
|
|
|
+ style={[
|
|
|
|
+ styles.fileNameText,
|
|
|
|
+ { color: leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT }
|
|
|
|
+ ]}
|
|
|
|
+ >
|
|
|
|
+ {fileName}
|
|
|
|
+ </Text>
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const renderMessageLocation = (props: BubbleProps<CustomMessage>) => {
|
|
|
|
+ const { currentMessage } = props;
|
|
|
|
+ if (!currentMessage?.attachment) return null;
|
|
|
|
+
|
|
|
|
+ const { lat, lng } = currentMessage.attachment;
|
|
|
|
+ if (!lat || !lng) return null;
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <View
|
|
|
|
+ style={[
|
|
|
|
+ {
|
|
|
|
+ alignItems: 'center',
|
|
|
|
+ borderRadius: 8,
|
|
|
|
+ marginVertical: 6,
|
|
|
|
+ marginHorizontal: 6,
|
|
|
|
+ width: 220
|
|
|
|
+ }
|
|
|
|
+ ]}
|
|
|
|
+ >
|
|
|
|
+ <MessageLocation props={props} lat={lat} lng={lng} onLongPress={handleLongPress} />
|
|
|
|
+ </View>
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const onShareLiveLocation = useCallback(() => {}, []);
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ let unsubscribe: any;
|
|
|
|
+
|
|
|
|
+ const setupNotificationHandler = async () => {
|
|
|
|
+ // todo: implement dismissChatNotifications
|
|
|
|
+ unsubscribe = await dismissChatNotifications(
|
|
|
|
+ group_token,
|
|
|
|
+ isSubscribed,
|
|
|
|
+ setModalInfo,
|
|
|
|
+ navigation
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ setupNotificationHandler();
|
|
|
|
+
|
|
|
|
+ return () => {
|
|
|
|
+ if (unsubscribe) unsubscribe();
|
|
|
|
+ updateUnreadMessagesCount();
|
|
|
|
+ };
|
|
|
|
+ }, [group_token]);
|
|
|
|
+
|
|
|
|
+ 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);
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // todo: implement dismissChatNotifications
|
|
|
|
+ await dismissChatNotifications(group_token, 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 === group_token && 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 === group_token && data.reaction) {
|
|
|
|
+ updateMessageWithReaction(data.reaction);
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case 'unreact':
|
|
|
|
+ if (data.conversation_with === group_token && data.unreacted_message_id) {
|
|
|
|
+ removeReactionFromMessage(data.unreacted_message_id);
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case 'delete_message':
|
|
|
|
+ if (data.conversation_with === group_token && data.deleted_message_id) {
|
|
|
|
+ removeDeletedMessage(data.deleted_message_id);
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case 'is_typing':
|
|
|
|
+ if (data.conversation_with === group_token) {
|
|
|
|
+ setIsTyping(true);
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case 'stopped_typing':
|
|
|
|
+ if (data.conversation_with === group_token) {
|
|
|
|
+ setIsTyping(false);
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case 'messages_read':
|
|
|
|
+ if (data.conversation_with === group_token && 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 === +currentUserId)
|
|
|
|
+ : [];
|
|
|
|
+ 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: group_token }));
|
|
|
|
+ } 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: group_token
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ 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: message.attachment ? message.attachment : -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,
|
|
|
|
+ // todo: sender_name
|
|
|
|
+ name: message.sender !== +currentUserId ? message.sender_name : 'Me',
|
|
|
|
+ avatar:
|
|
|
|
+ message.sender !== +currentUserId && message.sender_avatar
|
|
|
|
+ ? API_HOST + message.sender_avatar
|
|
|
|
+ : message.sender === +currentUserId
|
|
|
|
+ ? (null as never)
|
|
|
|
+ : undefined
|
|
|
|
+ },
|
|
|
|
+ replyMessage:
|
|
|
|
+ message.reply_to_id !== -1
|
|
|
|
+ ? {
|
|
|
|
+ text: message.reply_to.text,
|
|
|
|
+ id: message.reply_to.id,
|
|
|
|
+ name:
|
|
|
|
+ message.reply_to.sender !== +currentUserId ? message.reply_to?.sender_name : '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,
|
|
|
|
+ isSending: false,
|
|
|
|
+ video:
|
|
|
|
+ message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
|
|
|
|
+ ? API_HOST + message.attachement?.attachment_link
|
|
|
|
+ : null,
|
|
|
|
+ image:
|
|
|
|
+ message.attachement !== -1 && message.attachement?.filetype?.startsWith('image')
|
|
|
|
+ ? API_HOST + message.attachement?.attachment_small_url
|
|
|
|
+ : null
|
|
|
|
+ };
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ useFocusEffect(
|
|
|
|
+ useCallback(() => {
|
|
|
|
+ refetch();
|
|
|
|
+ }, [])
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ useFocusEffect(
|
|
|
|
+ useCallback(() => {
|
|
|
|
+ if (chatData?.groupAvatar) {
|
|
|
|
+ setGroupAvatar(API_HOST + chatData.groupAvatar);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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 !== +currentUserId
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ 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 !== +currentUserId &&
|
|
|
|
+ !sentToServer.current.has(item.item._id)
|
|
|
|
+ )
|
|
|
|
+ .map((item) => item.item._id);
|
|
|
|
+
|
|
|
|
+ if (newViewableUnreadMessages.length > 0) {
|
|
|
|
+ markMessagesAsRead(
|
|
|
|
+ {
|
|
|
|
+ token,
|
|
|
|
+ group_token,
|
|
|
|
+ messages_id: newViewableUnreadMessages
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ onSuccess: (res) => {
|
|
|
|
+ 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);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ // todo: delete api
|
|
|
|
+ const handleDeleteMessage = (messageId: number) => {
|
|
|
|
+ deleteMessage(
|
|
|
|
+ {
|
|
|
|
+ token,
|
|
|
|
+ message_id: messageId,
|
|
|
|
+ conversation_with_user: group_token
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ 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,
|
|
|
|
+ attachment: null,
|
|
|
|
+ image: undefined,
|
|
|
|
+ video: undefined
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ 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;
|
|
|
|
+ case 'download':
|
|
|
|
+ downloadFileToDevice(selectedMessage.currentMessage);
|
|
|
|
+ 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: group_token,
|
|
|
|
+ setMessages,
|
|
|
|
+ sendWebSocketMessage,
|
|
|
|
+ isGroup: true,
|
|
|
|
+ groupToken: group_token
|
|
|
|
+ } 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 !== +currentUserId ? reaction?.name : '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}
|
|
|
|
+ renderCustomView={() =>
|
|
|
|
+ selectedMessage.currentMessage.attachment?.filetype === 'nomadmania/location'
|
|
|
|
+ ? renderMessageLocation(selectedMessage)
|
|
|
|
+ : selectedMessage.currentMessage.attachment &&
|
|
|
|
+ !selectedMessage.currentMessage.image &&
|
|
|
|
+ !selectedMessage.currentMessage.video
|
|
|
|
+ ? renderMessageFile(selectedMessage)
|
|
|
|
+ : renderReplyMessageView(selectedMessage)
|
|
|
|
+ }
|
|
|
|
+ />
|
|
|
|
+ </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 !== +currentUserId ? replyMessage.user.name : 'Me'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ const user = {
|
|
|
|
+ _id: +currentUserId,
|
|
|
|
+ name: 'Me',
|
|
|
|
+ avatar: null
|
|
|
|
+ };
|
|
|
|
+ const message = { ...newMessages[0], pending: true, isSending: true, user };
|
|
|
|
+
|
|
|
|
+ setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
|
|
|
|
+
|
|
|
|
+ sendMessage(
|
|
|
|
+ {
|
|
|
|
+ token,
|
|
|
|
+ to_group_token: group_token,
|
|
|
|
+ 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, isSending: false } : msg
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ 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, group_token: group_token },
|
|
|
|
+ {
|
|
|
|
+ onSuccess: () => {
|
|
|
|
+ const message = messages.find((msg) => msg._id === messageId);
|
|
|
|
+ if (message) {
|
|
|
|
+ sendWebSocketMessage('new_reaction', message, reaction);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ 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 !== +currentUserId
|
|
|
|
+ ? 'rgba(255, 255, 255, 0.7)'
|
|
|
|
+ : 'rgba(0, 0, 0, 0.2)',
|
|
|
|
+ borderColor:
|
|
|
|
+ currentMessage.user._id !== +currentUserId ? 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 !== +currentUserId ? Colors.DARK_BLUE : Colors.WHITE
|
|
|
|
+ }
|
|
|
|
+ ]}
|
|
|
|
+ >
|
|
|
|
+ {currentMessage.replyMessage.name}
|
|
|
|
+ </Text>
|
|
|
|
+
|
|
|
|
+ <Text
|
|
|
|
+ numberOfLines={1}
|
|
|
|
+ style={[
|
|
|
|
+ styles.replyMessageText,
|
|
|
|
+ {
|
|
|
|
+ color: currentMessage.user._id !== +currentUserId ? 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;
|
|
|
|
+ const leftMessage = currentMessage?.user?._id !== +currentUserId;
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <TouchableOpacity
|
|
|
|
+ onPress={() => setSelectedMedia(API_HOST + currentMessage.attachment.attachment_full_url)}
|
|
|
|
+ onLongPress={() => handleLongPress(currentMessage, props)}
|
|
|
|
+ style={styles.imageContainer}
|
|
|
|
+ disabled={currentMessage.isSending}
|
|
|
|
+ >
|
|
|
|
+ <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
|
|
|
|
+ {currentMessage.isSending && (
|
|
|
|
+ <View
|
|
|
|
+ style={{
|
|
|
|
+ position: 'absolute',
|
|
|
|
+ top: 0,
|
|
|
|
+ left: 0,
|
|
|
|
+ right: 0,
|
|
|
|
+ bottom: 0,
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
+ alignItems: 'center'
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <ActivityIndicator
|
|
|
|
+ size="large"
|
|
|
|
+ color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
|
|
|
|
+ />
|
|
|
|
+ </View>
|
|
|
|
+ )}
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const renderTicks = (message: CustomMessage) => {
|
|
|
|
+ if (message.user._id !== +currentUserId) return null;
|
|
|
|
+
|
|
|
|
+ if (message.isSending) {
|
|
|
|
+ return (
|
|
|
|
+ <View>
|
|
|
|
+ <ActivityIndicator
|
|
|
|
+ size={16}
|
|
|
|
+ color={Colors.LIGHT_GRAY}
|
|
|
|
+ style={{ transform: 'scale(0.8)' }}
|
|
|
|
+ />
|
|
|
|
+ </View>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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;
|
|
|
|
+
|
|
|
|
+ const messageToCompare = props.previousMessage;
|
|
|
|
+
|
|
|
|
+ const showUserName =
|
|
|
|
+ props.position === 'left' &&
|
|
|
|
+ currentMessage &&
|
|
|
|
+ messageToCompare &&
|
|
|
|
+ (!isSameUser(currentMessage, messageToCompare) ||
|
|
|
|
+ !isSameDay(currentMessage, messageToCompare));
|
|
|
|
+
|
|
|
|
+ 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}
|
|
|
|
+ renderCustomView={() => {
|
|
|
|
+ return (
|
|
|
|
+ <View>
|
|
|
|
+ {showUserName ? (
|
|
|
|
+ <Text
|
|
|
|
+ style={{
|
|
|
|
+ color: Colors.BLACK,
|
|
|
|
+ fontWeight: '600',
|
|
|
|
+ fontSize: 13,
|
|
|
|
+ paddingHorizontal: 10,
|
|
|
|
+ paddingTop: 8,
|
|
|
|
+ paddingBottom: 2
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ {/* {'~ '} */}
|
|
|
|
+ {props.currentMessage.user.name}
|
|
|
|
+ </Text>
|
|
|
|
+ ) : null}
|
|
|
|
+ {currentMessage.attachment?.filetype === 'nomadmania/location'
|
|
|
|
+ ? renderMessageLocation(props)
|
|
|
|
+ : currentMessage.attachment && !currentMessage.image && !currentMessage.video
|
|
|
|
+ ? renderMessageFile(props)
|
|
|
|
+ : renderReplyMessageView(props)}
|
|
|
|
+ </View>
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ </View>
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const openAttachmentsModal = () => {
|
|
|
|
+ SheetManager.show('chat-attachments', {
|
|
|
|
+ payload: {
|
|
|
|
+ name: groupName,
|
|
|
|
+ uid: group_token,
|
|
|
|
+ setModalInfo,
|
|
|
|
+ closeOptions: () => {},
|
|
|
|
+ onSendMedia,
|
|
|
|
+ onSendLocation,
|
|
|
|
+ onShareLiveLocation,
|
|
|
|
+ onSendFile,
|
|
|
|
+ isGroup: true
|
|
|
|
+ } as any
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const renderInputToolbar = (props: any) => (
|
|
|
|
+ <InputToolbar
|
|
|
|
+ {...props}
|
|
|
|
+ renderActions={() =>
|
|
|
|
+ userType === 'normal' ? (
|
|
|
|
+ <Actions
|
|
|
|
+ icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
|
|
|
|
+ onPressActionButton={openAttachmentsModal}
|
|
|
|
+ />
|
|
|
|
+ ) : null
|
|
|
|
+ }
|
|
|
|
+ containerStyle={{
|
|
|
|
+ backgroundColor: Colors.FILL_LIGHT
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ 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%'
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <View style={{ paddingHorizontal: '5%' }}>
|
|
|
|
+ <Header
|
|
|
|
+ label={groupName}
|
|
|
|
+ textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
|
|
|
|
+ rightElement={
|
|
|
|
+ <TouchableOpacity
|
|
|
|
+ // todo: change to settings
|
|
|
|
+ onPress={
|
|
|
|
+ () => {}
|
|
|
|
+ // navigation.navigate(
|
|
|
|
+ // ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: 57363 }] as never)
|
|
|
|
+ // )
|
|
|
|
+ }
|
|
|
|
+ disabled={userType !== 'normal'}
|
|
|
|
+ >
|
|
|
|
+ {groupAvatar && userType === 'normal' ? (
|
|
|
|
+ <Image source={{ uri: groupAvatar }} style={styles.avatar} />
|
|
|
|
+ ) : userType === 'normal' ? (
|
|
|
|
+ <GroupIcon fill={Colors.DARK_BLUE} width={30} height={30} />
|
|
|
|
+ ) : (
|
|
|
|
+ <BanIcon fill={Colors.RED} width={30} height={30} />
|
|
|
|
+ )}
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
+ }
|
|
|
|
+ />
|
|
|
|
+ </View>
|
|
|
|
+
|
|
|
|
+ <GestureHandlerRootView style={styles.container}>
|
|
|
|
+ {messages ? (
|
|
|
|
+ <GiftedChat
|
|
|
|
+ messages={messages as CustomMessage[]}
|
|
|
|
+ listViewProps={{
|
|
|
|
+ ref: flatList,
|
|
|
|
+ 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}
|
|
|
|
+ showUserAvatar={true}
|
|
|
|
+ onPressAvatar={(user) => {
|
|
|
|
+ navigation.navigate(
|
|
|
|
+ ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: user._id }] as never)
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
|
|
+ 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={(props) => (
|
|
|
|
+ <RenderMessageVideo
|
|
|
|
+ props={props}
|
|
|
|
+ token={token}
|
|
|
|
+ currentUserId={+currentUserId}
|
|
|
|
+ onLongPress={handleLongPress}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ textInputProps={{
|
|
|
|
+ ...styles.composer,
|
|
|
|
+ selectionColor: Colors.LIGHT_GRAY
|
|
|
|
+ }}
|
|
|
|
+ placeholder=""
|
|
|
|
+ renderMessage={(props) => (
|
|
|
|
+ <ChatMessageBox
|
|
|
|
+ {...(props as MessageProps<CustomMessage>)}
|
|
|
|
+ updateRowRef={updateRowRef}
|
|
|
|
+ setReplyOnSwipeOpen={setReplyMessage}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ renderChatFooter={() => (
|
|
|
|
+ <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
|
|
|
|
+ )}
|
|
|
|
+ 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} />
|
|
|
|
+ )}
|
|
|
|
+
|
|
|
|
+ <ImageView
|
|
|
|
+ images={[{ uri: selectedMedia, cache: 'force-cache' }]}
|
|
|
|
+ imageIndex={0}
|
|
|
|
+ visible={!!selectedMedia}
|
|
|
|
+ onRequestClose={() => setSelectedMedia(null)}
|
|
|
|
+ backgroundColor={Colors.DARK_BLUE}
|
|
|
|
+ />
|
|
|
|
+
|
|
|
|
+ <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 />
|
|
|
|
+ </GestureHandlerRootView>
|
|
|
|
+ <View
|
|
|
|
+ style={{
|
|
|
|
+ height: insets.bottom,
|
|
|
|
+ backgroundColor: Colors.FILL_LIGHT
|
|
|
|
+ }}
|
|
|
|
+ />
|
|
|
|
+ </SafeAreaView>
|
|
|
|
+ );
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+export default GroupChatScreen;
|