import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import {
  View,
  TouchableOpacity,
  Image,
  Text,
  FlatList,
  Dimensions,
  Alert,
  ScrollView,
  Linking,
  ActivityIndicator,
  AppState,
  AppStateStatus,
  TextInput,
  Platform,
  Keyboard
} from 'react-native';
import {
  GiftedChat,
  Bubble,
  InputToolbar,
  IMessage,
  Send,
  BubbleProps,
  Composer,
  TimeProps,
  MessageProps,
  Actions,
  isSameUser,
  isSameDay,
  SystemMessage,
  ComposerProps
} from 'react-native-gifted-chat';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
import { CustomImageViewer, Header, WarningModal } from 'src/components';
import { Colors } from 'src/theme';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { setAudioModeAsync } from 'expo-audio';
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 {
  usePostGetGroupChatQuery,
  usePostGetPinnedGroupMessageQuery,
  usePostSetPinGroupMessageMutation,
  usePostGetGroupSettingsQuery,
  usePostGetGroupMembersQuery
} from '@api/chat';
import { CustomMessage, Reaction } from '../types';
import { API_HOST, APP_VERSION, WEBSOCKET_URL } from 'src/constants';
import ReactionBar from '../Components/ReactionBar';
import OptionsMenu from '../Components/OptionsMenu';
import EmojiSelectorModal from '../Components/EmojiSelectorModal';
import TypingIndicator from '../Components/TypingIndicator';
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 {
  compressImageWithProgress,
  compressVideoWithProgress,
  dismissChatNotifications,
  downloadImageFromViewer,
  isMessageEdited
} from '../utils';
import { useMessagesStore } from 'src/stores/unreadMessagesStore';
import FileViewer from 'react-native-file-viewer';
import { FileSystem } from 'src/utils';
import Share from 'react-native-share';

import BanIcon from 'assets/icons/messages/ban.svg';
import AttachmentsModal from '../Components/AttachmentsModal';
import RenderMessageVideo from '../Components/renderMessageVideo';
import RenderMessageImage from '../Components/RenderMessageImage';
import MessageLocation from '../Components/MessageLocation';
import GroupIcon from 'assets/icons/messages/group-chat.svg';
import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
import GroupStatusModal from '../Components/GroupStatusModal';
import PinIcon from 'assets/icons/messages/pin.svg';
import MentionsList from '../Components/MentionsList';
import { useConnection } from 'src/contexts/ConnectionContext';
import { useMessagesLive } from 'src/watermelondb/features/chat/hooks/useChatThread';
import { Message } from 'src/watermelondb/models';
import {
  addMessageDirtyAction,
  OutgoingWsEvent,
  reconcileChatRange,
  triggerMessagePush,
  upsertMessagesIntoDB
} from 'src/watermelondb/features/chat/data/message.sync';
import { database } from 'src/watermelondb';
import { Q } from '@nozbe/watermelondb';
import { createOptimisticMessage } from 'src/watermelondb/features/chat/data/createOptimisticMessage';
import _ from 'lodash';

const options = {
  enableVibrateFallback: true,
  ignoreAndroidSystemSettings: false
};

const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];

export async function findGroupMsgRecord(id: number, groupToken: string): Promise<Message | null> {
  const messagesCollection = database.get<Message>('messages');

  const res = await messagesCollection
    .query(Q.where('chat_key', 'g:' + groupToken), Q.where('message_id', id))
    .fetch();
  return res[0] ?? null;
}

const GroupChatScreen = ({ route }: { route: any }) => {
  const token = storage.get('token', StoreType.STRING) as string;
  const [isConnected, setIsConnected] = useState<boolean>(true);
  const netInfo = useConnection();

  const {
    group_token,
    name,
    avatar,
    userType = 'normal',
    announcement,
    canSendMessages
  }: {
    group_token: string;
    name: string;
    avatar: string | null;
    userType: 'normal' | 'not_exist' | 'blocked';
    announcement: 0 | 1;
    canSendMessages: 0 | 1;
  } = 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 navigation = useNavigation();
  const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
  const [visibleBeforeId, setVisibleBeforeId] = useState<number | null>(null);
  const {
    data: chatData,
    refetch,
    isFetching,
    isFetchedAfterMount,
    isRefetching
  } = usePostGetGroupChatQuery(token, group_token, 50, prevThenMessageId, true);
  const [canSeeMembers, setCanSeeMembers] = useState(false);

  const { data: pinData, refetch: refetchPinned } = usePostGetPinnedGroupMessageQuery(
    token,
    group_token,
    true
  );
  const { data } = usePostGetGroupSettingsQuery(token, group_token, true);
  const { data: members, refetch: refetchMembers } = usePostGetGroupMembersQuery(
    token,
    group_token,
    canSeeMembers
  );

  const [storedMembers, setStoredMembers] = useState<any>(null);
  const [isSearchingMessage, setIsSearchingMessage] = useState<number | null>(null);

  const swipeableRowRef = useRef<Swipeable | null>(null);
  const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
  const [selectedMedia, setSelectedMedia] = useState<any>(null);

  const [pinned, setPinned] = 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: pinMessage } = usePostSetPinGroupMessageMutation();

  const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
  const [isRerendering, setIsRerendering] = useState<boolean>(false);
  const [isTyping, setIsTyping] = useState<string | null>(null);

  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 [insetsColor, setInsetsColor] = useState(Colors.FILL_LIGHT);

  const [text, setText] = useState('');
  const [mentionList, setMentionList] = useState<any>([]);
  const [showMentions, setShowMentions] = useState(false);
  const [inputHeight, setInputHeight] = useState(45);
  const [editingMessage, setEditingMessage] = useState<CustomMessage | null>(null);
  const [cacheKey, setCacheKey] = useState(Date.now());
  const [extraMessages, setExtraMessages] = useState<Message[]>([]);

  const { scrollToMessageId } = route.params ?? {};

  const allMessages = useMessagesLive({
    groupChatToken: group_token,
    limit: 50,
    aroundMessageId: scrollToMessageId
  });

  const appState = useRef(AppState.currentState);
  const textInputRef = useRef<TextInput>(null);

  const socket = useRef<WebSocket | null>(null);
  const scrollViewRef = useRef<ScrollView>(null);

  const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
    const attachment =
      message.attachment && message.attachment !== '-1' ? JSON.parse(message.attachment) : null;

    const replyData =
      message.replyToId && message.replyToId !== -1 && message.replyTo
        ? JSON.parse(message.replyTo)
        : '{}';

    return {
      _id: message.messageId ?? message.id,
      text: message.text ?? '',
      createdAt: new Date(message.sentAt + 'Z'),
      user: {
        _id: message.senderId,
        name: message.senderId !== +currentUserId ? message.senderName : 'Me',
        avatar:
          message.senderId !== +currentUserId && message.senderAvatar
            ? API_HOST + message.senderAvatar
            : message.senderId === +currentUserId
              ? (null as never)
              : undefined
      },
      replyMessage:
        message.replyToId && message.replyToId !== -1 && replyData
          ? {
              id: replyData.id,
              text: replyData.text,
              name: replyData.sender !== +currentUserId ? replyData.sender_name : 'Me'
            }
          : null,
      reactions: JSON.parse(message.reactions || '{}'),
      attachment,
      pending: message.status === 1,
      sent: message.status === 2,
      received: message.status === 3,
      deleted: message.status === 4,
      edited: message.edits ? isMessageEdited(message.edits) : false,
      isSending: message?.isSending ? message.isSending : false,
      image:
        attachment && attachment?.filetype?.startsWith('image')
          ? attachment.filetype === 'image/processing'
            ? attachment?.local_uri
            : API_HOST + attachment?.attachment_small_url
          : null,
      video:
        attachment && attachment?.filetype?.startsWith('video')
          ? attachment.filetype === 'video/processing'
            ? attachment?.local_uri
            : API_HOST + attachment?.attachment_link
          : null,
      system: message.senderId === -1
    };
  };

  const giftedMessages = useMemo(
    () => [
      ...allMessages.map(mapApiMessageToGiftedMessage),
      ...extraMessages.map(mapApiMessageToGiftedMessage)
    ],
    [allMessages, extraMessages]
  );

  useEffect(() => {
    if (!scrollToMessageId) return;
    if (!giftedMessages.length) return;

    const index = giftedMessages.findIndex((m) => m._id === scrollToMessageId);

    if (index === -1) return;

    setTimeout(() => {
      requestAnimationFrame(() => {
        flatList.current?.scrollToIndex({
          index,
          animated: false,
          viewPosition: 0.5
        });
      });
    }, 500);
  }, [giftedMessages, scrollToMessageId]);

  useEffect(() => {
    if (isModalVisible) {
      setTimeout(() => {
        scrollViewRef.current?.scrollToEnd({ animated: false });
      }, 50);
    }
  }, [isModalVisible]);

  useFocusEffect(
    useCallback(() => {
      setCacheKey(Date.now());
    }, [navigation])
  );

  const closeModal = () => {
    setModalInfo({ ...modalInfo, visible: false });
  };

  useEffect(() => {
    setAudioModeAsync({
      allowsRecording: false,
      playsInSilentMode: true,
      interruptionModeAndroid: 'duckOthers',
      interruptionMode: 'mixWithOthers'
    });
  }, []);

  const [isKeyboardVisible, setKeyboardVisible] = useState(false);

  useEffect(() => {
    const keyboardWillShow = Keyboard.addListener(
      Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
      () => setKeyboardVisible(true)
    );
    const keyboardWillHide = Keyboard.addListener(
      Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
      () => setKeyboardVisible(false)
    );

    return () => {
      keyboardWillShow.remove();
      keyboardWillHide.remove();
    };
  }, []);

  useEffect(() => {
    if (pinData && pinData?.message) {
      setPinned(pinData.message);
    }
  }, [pinData]);

  const onSendMedia = useCallback(
    async (files: { uri: string; type: 'image' | 'video' }[]) => {
      const formatedReply = replyMessage
        ? {
            text: replyMessage.text,
            id: replyMessage._id,
            name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me',
            sender: replyMessage.user._id,
            sender_name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
          }
        : null;

      for (const file of files) {
        const optimisticId = await createOptimisticMessage({
          groupToken: group_token,
          currentUserId: +currentUserId,
          uiAttachment: {
            id: -1,
            filename: file.type,
            filetype: file.type === 'image' ? 'image/processing' : 'video/processing',
            local_uri: file.uri,
            attachment_link: file.uri,
            attachment_full_url: file.uri
          },
          sendAttachment: {
            uri: file.uri,
            type: file.type,
            name: file.uri.split('/').pop()
          },
          replyMessage: formatedReply,
          shouldAddDirty: false
        });

        const updateProgress = async (progress: number) => {};

        let compressedUri = file.uri;

        if (file.type === 'image') {
          compressedUri = await compressImageWithProgress(file.uri, updateProgress);
        }

        if (file.type === 'video') {
          compressedUri = await compressVideoWithProgress(file.uri, updateProgress);
        }

        const optimisticMessage = await findGroupMsgRecord(optimisticId, group_token);

        if (optimisticMessage) {
          await database.write(async () => {
            optimisticMessage.update((m) => {
              m.attachment = JSON.stringify({
                ...JSON.parse(m.attachment ?? '{}'),
                local_uri: compressedUri
              });

              addMessageDirtyAction(optimisticMessage, {
                type: 'send',
                value: {
                  text,
                  currentUid: currentUserId,
                  attachment: {
                    uri: compressedUri,
                    type: file.type,
                    name: file.uri.split('/').pop()
                  },
                  reply_to_id: formatedReply ? formatedReply.id : -1,
                  replyMessage: formatedReply
                }
              });
            });
          });
        }
      }

      clearReplyMessage();
      await triggerMessagePush(token, sendWsEvent);
    },
    [replyMessage]
  );

  const onSendLocation = useCallback(
    async (coords: { latitude: number; longitude: number }) => {
      const locationData = JSON.stringify({ lat: coords.latitude, lng: coords.longitude });
      const fileUri = FileSystem.documentDirectory + 'location.json';
      await FileSystem.writeAsStringAsync(fileUri, locationData);

      const formatedReply = replyMessage
        ? {
            text: replyMessage.text,
            id: replyMessage._id,
            name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me',
            sender: replyMessage.user._id,
            sender_name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
          }
        : null;

      await createOptimisticMessage({
        groupToken: group_token,
        currentUserId: +currentUserId,
        uiAttachment: {
          id: -1,
          filename: 'location.json',
          filetype: 'nomadmania/location',
          lat: coords.latitude,
          lng: coords.longitude
        },
        sendAttachment: {
          uri: fileUri,
          type: 'application/json',
          name: 'location.json'
        },
        replyMessage: formatedReply
      });
      clearReplyMessage();

      await triggerMessagePush(token, sendWsEvent);
    },
    [replyMessage]
  );

  const onSendFile = useCallback(
    async (files: { uri: string; type: string; name?: string }[]) => {
      const formatedReply = replyMessage
        ? {
            text: replyMessage.text,
            id: replyMessage._id,
            name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me',
            sender: replyMessage.user._id,
            sender_name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
          }
        : null;

      for (const file of files) {
        await createOptimisticMessage({
          groupToken: group_token,
          currentUserId: +currentUserId,
          uiAttachment: {
            id: -1,
            filename: file.name ?? 'File',
            filetype: file.type,
            attachment_link: file.uri
          },
          sendAttachment: {
            uri: file.uri,
            type: file.type,
            name: file.name || file.uri.split('/').pop()
          },
          replyMessage: formatedReply
        });
      }
      clearReplyMessage();

      await triggerMessagePush(token, sendWsEvent);
    },
    [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 && fileExists.size > 1024) {
        await FileViewer.open(fileUri, {
          showOpenWithDialog: true,
          showAppsSuggestions: true
        });

        return;
      }

      const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
        headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
      });

      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 || 'application/octet-stream';
    let fileExt = fileType.split('/').pop() || (currentMessage.video ? 'mp4' : 'jpg');

    if (Platform.OS === 'android' && fileType === 'video/quicktime') {
      fileExt = 'mp4';
    }

    const fileName = currentMessage.attachment?.filename?.split('.')[0] || 'file';
    const fileUri = `${FileSystem.cacheDirectory}${fileName}.${fileExt}`;

    try {
      const downloadOptions = {
        headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
      };

      const { uri } = await FileSystem.downloadAsync(fileUrl, fileUri, downloadOptions);

      await Share.open({
        url: uri,
        type: fileType,
        failOnCancel: false
      });
    } 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(() => {
    if (data && data.settings) {
      setCanSeeMembers(data.settings.members_can_see_members === 1 || data.settings.admin === 1);
      storage.set(
        `canSeeMembers-${group_token}`,
        data.settings.members_can_see_members === 1 || data.settings.admin === 1
      );
    } else {
      const parsedData =
        (storage.get(`canSeeMembers-${group_token}`, StoreType.BOOLEAN) as boolean) ?? true;
      setCanSeeMembers(parsedData);
    }
  }, [data]);

  useEffect(() => {
    if (members && members.settings) {
      setStoredMembers(members.settings);
      storage.set(`members-${group_token}`, JSON.stringify(members.settings));
    } else {
      const parsedMembers = JSON.parse(
        (storage.get(`members-${group_token}`, StoreType.STRING) as string) ?? '[]'
      );
      setStoredMembers(parsedMembers);
    }
  }, [members]);

  useEffect(() => {
    let unsubscribe: any;

    const setupNotificationHandler = async () => {
      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) => {
      try {
        const data = JSON.parse(event.data);
        handleWebSocketMessage(data);
      } catch {
        console.log('Invalid WS message:', event.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) => {
      const prevState = appState.current;
      appState.current = nextAppState;

      if (prevState.match(/inactive|background/) && nextAppState === 'active') {
        refetch();

        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) => {
            try {
              const data = JSON.parse(event.data);
              handleWebSocketMessage(data);
            } catch {
              console.log('Invalid WS message:', event.data);
            }
          };
        }

        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 = async (data: any) => {
    switch (data.action) {
      case 'new_message':
        if (data.group_token === group_token && data.message && data.uid !== +currentUserId) {
          await upsertMessagesIntoDB({
            groupToken: group_token,
            apiMessages: [data.message],
            avatar: data.avatar,
            name: data.name
          });
        }
        break;

      case 'new_reaction':
        if (data.group_token === group_token && data.reaction && data.uid !== +currentUserId) {
          const record = await findGroupMsgRecord(data.reaction.message_id, group_token);

          if (!record) return;

          await database.write(async () => {
            record.update((m) => {
              const current = m.reactions ? JSON.parse(m.reactions) : [];

              m.reactions = JSON.stringify([
                ...(Array.isArray(current)
                  ? current?.filter((r: any) => r.uid !== data.reaction.uid)
                  : [])
              ]);
            });
          });
        }
        break;

      case 'unreact':
        if (
          data.group_token === group_token &&
          data.unreacted_message_id &&
          data.uid !== +currentUserId
        ) {
          const record = await findGroupMsgRecord(data.unreacted_message_id, group_token);

          if (!record) return;

          await database.write(async () => {
            record.update((m) => {
              const current = m.reactions ? JSON.parse(m.reactions) : [];

              m.reactions = JSON.stringify(
                Array.isArray(current) ? current?.filter((r: any) => r.uid === +currentUserId) : []
              );
            });
          });
        }
        break;

      case 'delete_message':
        if (
          data.group_token === group_token &&
          data.deleted_message_id &&
          data.uid !== +currentUserId
        ) {
          const record = await findGroupMsgRecord(data.deleted_message_id, group_token);
          if (!record) return;

          await database.write(async () => {
            record.update((m) => {
              m.status = 4;
              m.text = 'This message was deleted';
              m.attachment = null;
              m.replyTo = null;
              m.replyToId = -1;
            });
          });
        }
        break;

      case 'is_typing':
        if (data.group_token === group_token && data.uid !== +currentUserId) {
          setIsTyping(data.name);
        }
        break;

      case 'stopped_typing':
        if (data.group_token === group_token) {
          setIsTyping(null);
        }
        break;

      case 'messages_read':
        const readIds = data.read_messages_ids;
        if (
          data.group_token === group_token &&
          Array.isArray(readIds) &&
          readIds.length &&
          data.uid !== +currentUserId
        ) {
          const records = await database
            .get<Message>('messages')
            .query(Q.where('chat_key', 'g:' + group_token), Q.where('message_id', Q.oneOf(readIds)))
            .fetch();

          if (!records.length) return;

          await database.write(async () => {
            records.forEach((msg: Message) => {
              msg.update((r) => {
                r.status = 3;
                (r as any)._raw._status = 'synced';
                (r as any)._raw._changed = '';
              });
            });
          });
        }
        break;
      case 'messages_received':
        const receivedIds = data.received_messages_ids;
        if (
          data.group_token === group_token &&
          Array.isArray(receivedIds) &&
          receivedIds.length &&
          data.uid !== +currentUserId
        ) {
          const records = await database
            .get<Message>('messages')
            .query(
              Q.where('chat_key', 'g:' + group_token),
              Q.where('message_id', Q.oneOf(receivedIds))
            )
            .fetch();

          if (!records.length) return;

          await database.write(async () => {
            records.forEach((r) => {
              r.update((m) => {
                m.status = 2;
                m.isSending = false;
                (m as any)._raw._status = 'synced';
                (m as any)._raw._changed = '';
              });
            });
          });
        }
        break;

      case 'edited_message':
        if (data.group_token === group_token && data.message && data.uid !== +currentUserId) {
          const record = await findGroupMsgRecord(data.message.id, group_token);

          if (!record) return;

          await database.write(async () => {
            record.update((m) => {
              m.text = data.message.text;
              m.edits = '[{}]';
            });
          });
        }
        break;

      default:
        break;
    }
  };

  useEffect(() => {
    const pingInterval = setInterval(() => {
      if (socket.current && socket.current.readyState === WebSocket.OPEN) {
        socket.current.send(
          JSON.stringify({ action: 'ping', conversation_with_group: group_token })
        );
      } else {
        socket.current = new WebSocket(WEBSOCKET_URL);
        socket.current.onopen = () => {
          socket.current?.send(JSON.stringify({ token }));
        };
        socket.current.onmessage = (event) => {
          try {
            const data = JSON.parse(event.data);
            handleWebSocketMessage(data);
          } catch {
            console.log('Invalid WS message:', event.data);
          }
        };

        return () => {
          if (socket.current) {
            socket.current.close();
            socket.current = null;
          }
        };
      }
    }, 50000);

    return () => clearInterval(pingInterval);
  }, []);

  const sendWsEvent = useCallback((event: OutgoingWsEvent) => {
    if (!socket.current) return;
    if (socket.current.readyState !== WebSocket.OPEN) return;

    if (event.action === 'new_message' && event.payload) {
      socket.current.send(
        JSON.stringify({
          action: event.action,
          conversation_with_group: group_token,
          message: {
            id: event.payload.message._id,
            text: event.payload.message.text,
            sender: +currentUserId,
            sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19),
            reply_to_id: event.payload.message.replyMessage?.id ?? -1,
            reply_to: event.payload.message.replyMessage ?? null,
            reactions: event.payload.message.reactions ?? '{}',
            status: 2,
            attachement: event.payload.message.attachment ? event.payload.message.attachment : -1
          }
        })
      );
    }
  }, []);

  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: group_token
      };

      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;
      }

      if (action === 'edited_message' && message) {
        data.message = {
          id: message._id,
          text: message.text
        };
      }

      socket.current.send(JSON.stringify(data));
    }
  };

  const handleTyping = (isTyping: boolean) => {
    if (isTyping) {
      sendWebSocketMessage('is_typing');
    } else {
      sendWebSocketMessage('stopped_typing');
    }
  };

  useFocusEffect(
    useCallback(() => {
      refetch();
    }, [])
  );

  const didInitUnreadRef = useRef(false);

  useEffect(() => {
    if (chatData?.groupAvatar) {
      setGroupAvatar(API_HOST + chatData.groupAvatar);
    }

    if (didInitUnreadRef.current) return;
    if (!giftedMessages.length) return;
    if (!chatData?.messages?.length) return;
    if (isFetching) return;

    const firstUnreadIndexFromServer = chatData.messages
      .slice()
      .reverse()
      .findIndex(
        (msg) =>
          msg.status !== 3 && msg.status !== 4 && msg.sender !== +currentUserId && msg.sender !== -1
      );

    if (firstUnreadIndexFromServer === -1) {
      didInitUnreadRef.current = true;
      setUnreadMessageIndex(null);
      return;
    }

    const unreadMessageId =
      chatData.messages[chatData.messages.length - 1 - firstUnreadIndexFromServer]?.id;

    if (!unreadMessageId) return;

    didInitUnreadRef.current = true;
    setUnreadMessageIndex(unreadMessageId);

    const giftedIndex = giftedMessages.findIndex((m) => m._id === unreadMessageId);

    if (giftedIndex !== -1) {
      setTimeout(() => {
        flatList.current?.scrollToIndex({
          index: giftedIndex,
          animated: true,
          viewPosition: 0.5
        });
      }, 400);
    }
  }, [giftedMessages, chatData]);

  const giftedMessagesWithUnread = useMemo(() => {
    if (unreadMessageIndex == null || unreadMessageIndex === -1) {
      return giftedMessages;
    }

    const index = giftedMessages.findIndex((m) => m._id === unreadMessageIndex);

    if (index === -1) return giftedMessages;

    const unreadMarker = {
      _id: 'unreadMarker',
      text: 'Unread messages',
      system: true
    };

    const copy = [...giftedMessages];
    copy.splice(index + 1, 0, unreadMarker as any);

    return copy;
  }, [giftedMessages, unreadMessageIndex]);

  const reconcileDebounced = useMemo(() => _.debounce(reconcileChatRange, 300), []);

  useEffect(() => {
    return () => {
      reconcileDebounced.cancel();
    };
  }, []);

  useEffect(() => {
    if (!isFetchedAfterMount) {
      if (!isRefetching) {
        refetch();
      }
      return;
    }
    if (!chatData?.messages?.length) {
      setHasMoreMessages(false);
      return;
    }

    upsertMessagesIntoDB({ groupToken: group_token, apiMessages: chatData.messages });

    reconcileDebounced(`g:${group_token}`, chatData.messages, Boolean(prevThenMessageId < 0));

    setVisibleBeforeId(prevThenMessageId);

    if (chatData.messages.length < 50) {
      setHasMoreMessages(false);
    }
  }, [chatData, isRefetching]);

  useEffect(() => {
    if (giftedMessages) {
      if (isSearchingMessage) {
        const messageIndex = giftedMessages.findIndex((msg) => msg._id === isSearchingMessage);

        if (messageIndex !== -1 && flatList.current) {
          setIsSearchingMessage(null);
        }
        scrollToMessage(isSearchingMessage);
      }
    }
  }, [giftedMessages]);

  const loadEarlierMessages = async () => {
    if ((isLoadingEarlier && !isSearchingMessage) || !hasMoreMessages || !giftedMessages) return;

    const oldest = giftedMessages[giftedMessages.length - 1];

    if (!oldest?._id) return;
    setPrevThenMessageId(oldest._id);
  };

  useEffect(() => {
    const getExtraData = async () => {
      setIsLoadingEarlier(true);

      const chatKey = `g:${group_token}`;

      const older = await database
        .get<Message>('messages')
        .query(
          Q.where('chat_key', chatKey),
          Q.where('message_id', Q.lt(visibleBeforeId as number)),
          Q.sortBy('sent_at', Q.desc),
          Q.take(50)
        )
        .fetch();

      if (!older.length) {
        setHasMoreMessages(false);
      } else {
        setExtraMessages((prev) => {
          const existingIds = new Set([...allMessages, ...prev].map((m) => m.messageId));

          const deduped = older.filter((m) => !existingIds.has(m.messageId));

          return [...prev, ...deduped];
        });
      }

      setIsLoadingEarlier(false);
    };

    if (visibleBeforeId && visibleBeforeId !== -1) {
      getExtraData();
    }
  }, [visibleBeforeId]);

  const sentToServer = useRef<Set<number>>(new Set());

  const handleViewableItemsChanged = _.throttle(
    async ({ viewableItems }: { viewableItems: any[] }) => {
      const newViewableUnreadMessages = viewableItems
        .filter(
          (item) =>
            !item.item.received &&
            !item.item.deleted &&
            item.item._id !== 'unreadMarker' &&
            item.item.user._id !== +currentUserId &&
            !sentToServer.current.has(item.item._id)
        )
        .map((item) => item.item._id);

      if (newViewableUnreadMessages.length > 0) {
        const messagesToUpdate = await database
          .get<Message>('messages')
          .query(
            Q.where('chat_key', 'g:' + group_token),
            Q.where('message_id', Q.oneOf(newViewableUnreadMessages))
          )
          .fetch();

        if (messagesToUpdate.length > 0) {
          await database.write(async () => {
            messagesToUpdate.forEach((msg: Message) => {
              msg.update((r) => {
                r.status = 3;
                addMessageDirtyAction(r, {
                  type: 'read',
                  value: { messagesIds: [msg.messageId] }
                });
              });
              sentToServer.current.add(msg.messageId as number);
            });
          });

          await triggerMessagePush(token, sendWsEvent);
        }

        sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages);
      }
    },
    1000
  );

  const renderSystemMessage = (props: any) => {
    if (props.currentMessage._id === 'unreadMarker') {
      return (
        <View style={styles.unreadMessagesContainer}>
          <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
        </View>
      );
    } else if (props.currentMessage.user._id === -1) {
      return (
        <SystemMessage
          currentMessage={props.currentMessage}
          containerStyle={{
            marginTop: 0,
            marginBottom: 0,
            paddingVertical: 2,
            paddingHorizontal: 12
          }}
          wrapperStyle={{
            backgroundColor: Colors.FILL_LIGHT,
            paddingHorizontal: 6,
            paddingVertical: 4,
            borderRadius: 10
          }}
          textStyle={{
            color: Colors.DARK_BLUE,
            fontStyle: 'italic',
            fontSize: 12,
            textAlign: 'center'
          }}
        />
      );
    }
    return null;
  };

  const clearReplyMessage = () => setReplyMessage(null);

  const clearEditMessage = () => {
    setEditingMessage(null);
    setText('');
  };

  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;
        }

        const maxSpaceBelow = isMine ? 280 : 200;
        if (spaceBelow < maxSpaceBelow) {
          const extraShift = maxSpaceBelow - spaceBelow;
          finalY -= extraShift;
        }

        if (spaceAbove < 50) {
          const extraShift = 50 - spaceAbove;
          finalY += extraShift;
        }

        if (spaceBelow < 220 || 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 = async (messageId: number) => {
    const existingMsg = await findGroupMsgRecord(messageId, group_token);

    if (existingMsg) {
      await database.write(async () => {
        existingMsg.update((r) => {
          r.status = 4;
          r.text = 'This message was deleted';
          r.attachment = null;
          r.replyToId = null;
          addMessageDirtyAction(r, {
            type: 'delete'
          });
        });
      });
      await triggerMessagePush(token, sendWsEvent);
    }

    const messageToDelete = giftedMessages?.find((msg) => msg._id === messageId);
    sendWebSocketMessage('delete_message', messageToDelete, null, null);
  };

  const handlePinMessage = (messageId: number, pin: 0 | 1) => {
    pinMessage(
      {
        token,
        message_id: messageId,
        group_token,
        pin
      },
      {
        onSuccess: () => {
          refetchPinned();
          if (pin === 0) {
            setPinned(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':
        setIsModalVisible(false);

        setTimeout(() => {
          downloadFileToDevice(selectedMessage.currentMessage);
        }, 300);
        break;
      case 'info':
        SheetManager.show('group-status', {
          payload: {
            messageId: selectedMessage.currentMessage._id,
            groupToken: group_token,
            setInsetsColor
          } as any
        });
        setIsModalVisible(false);
        setInsetsColor(Colors.WHITE);
        break;
      case 'pin':
        handlePinMessage(selectedMessage.currentMessage?._id, 1);
        setIsModalVisible(false);
        break;
      case 'edit':
        handleEditMessage(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,
        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}>
          {time.currentMessage.edited && <Text style={styles.timeText}>Edited</Text>}
          <Text style={[styles.timeText, time.currentMessage.edited ? { paddingLeft: 0 } : {}]}>
            {formattedTime}
          </Text>
          {renderTicks(time.currentMessage)}
        </View>
      </View>
    );
  };

  const renderSelectedMessage = () => {
    if (!selectedMessage) return;

    const messageToCompare = selectedMessage.previousMessage;

    const showUserName =
      selectedMessage.position === 'left' &&
      selectedMessage.currentMessage &&
      messageToCompare &&
      (!isSameUser(selectedMessage.currentMessage, messageToCompare) ||
        !isSameDay(selectedMessage.currentMessage, messageToCompare));

    return (
      <View
        style={{
          left: !messagePosition?.isMine && messagePosition?.x ? messagePosition?.x - 8 : undefined,
          top: messagePosition?.y && messagePosition?.y > 120 ? messagePosition?.y : 0,
          overflow: 'hidden'
        }}
      >
        <ScrollView
          ref={scrollViewRef}
          contentContainerStyle={{
            paddingBottom: 60
          }}
          showsVerticalScrollIndicator={false}
        >
          <Bubble
            {...selectedMessage}
            wrapperStyle={{
              right: {
                backgroundColor: Colors.DARK_BLUE,
                marginTop: messagePosition?.y && messagePosition?.y > 120 ? 0 : 120,
                marginRight: 8
              },
              left: {
                backgroundColor: Colors.FILL_LIGHT,
                marginTop: messagePosition?.y && messagePosition?.y > 120 ? 0 : 120,
                marginLeft: 8
              }
            }}
            textStyle={{
              right: { color: Colors.WHITE },
              left: { color: Colors.DARK_BLUE }
            }}
            renderTicks={() => null}
            renderTime={renderTimeContainer}
            renderCustomView={() => (
              <View>
                {showUserName ? (
                  <Text
                    style={{
                      color: Colors.BLACK,
                      fontWeight: '600',
                      fontSize: 13,
                      paddingHorizontal: 10,
                      paddingTop: 8,
                      paddingBottom: 2
                    }}
                  >
                    {selectedMessage.currentMessage.user.name}
                  </Text>
                ) : null}
                {selectedMessage.currentMessage.attachment?.filetype === 'nomadmania/location'
                  ? renderMessageLocation(selectedMessage)
                  : selectedMessage.currentMessage.attachment &&
                      !selectedMessage.currentMessage.image &&
                      !selectedMessage.currentMessage.video
                    ? renderMessageFile(selectedMessage)
                    : renderReplyMessageView(selectedMessage)}
              </View>
            )}
          />
          <OptionsMenu
            selectedMessage={selectedMessage}
            handleOptionPress={handleOptionPress}
            messagePosition={messagePosition}
            isGroup={true}
            isAdmin={data?.settings?.admin == 1}
            isAnnouncement={announcement === 1}
          />
        </ScrollView>
      </View>
    );
  };

  const handleBackgroundPress = () => {
    setIsModalVisible(false);
    setSelectedMessage(null);
    closeEmojiSelector();
  };

  useFocusEffect(
    useCallback(() => {
      navigation?.getParent()?.setOptions({
        tabBarStyle: {
          display: 'none'
        }
      });
    }, [navigation])
  );

  const replaceMentionsWithNames = (text: string) => {
    const userList = storedMembers ?? [];

    return text.replace(/@\{(\d+)\}/g, (_, uid) => {
      const user = userList.find((m: any) => m.uid === +uid);
      return user ? `@${user.name}` : `@{${uid}}`;
    });
  };

  const handleEditMessage = (message: CustomMessage) => {
    setReplyMessage(null);
    setEditingMessage({ ...message, text: replaceMentionsWithNames(message.text) });

    setText(replaceMentionsWithNames(message.text));
    textInputRef.current?.focus();
  };

  const onSend = useCallback(
    async (newMessages: CustomMessage[] = []) => {
      if (editingMessage) {
        if (editingMessage.text !== newMessages[0].text) {
          const editedText = transformMessageForServer(newMessages[0].text);
          const existingMsg = await findGroupMsgRecord(editingMessage._id, group_token);

          if (existingMsg) {
            await database.write(async () => {
              existingMsg.update((r) => {
                r.text = editedText;
                r.edits = '[{}]';
                addMessageDirtyAction(r, {
                  type: 'edit',
                  value: {
                    text: editedText
                  }
                });
              });
            });

            clearEditMessage();
            clearReplyMessage();
            await triggerMessagePush(token, sendWsEvent);
          }

          const editedMessage = {
            _id: editingMessage._id,
            text: editedText
          };
          sendWebSocketMessage('edited_message', editedMessage as unknown as CustomMessage);
        }

        return;
      }

      const msg = newMessages[0];

      const formatedReply = replyMessage
        ? {
            text: transformMessageForServer(replyMessage.text),
            id: replyMessage._id,
            name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me',
            sender: replyMessage.user._id,
            sender_name: replyMessage.user._id !== +currentUserId ? replyMessage.user.name : 'Me'
          }
        : null;

      await createOptimisticMessage({
        groupToken: group_token,
        currentUserId: +currentUserId,
        text: transformMessageForServer(msg.text),
        replyMessage: formatedReply
      });

      clearReplyMessage();

      await triggerMessagePush(token, sendWsEvent);
    },
    [replyMessage, editingMessage, isConnected, storedMembers]
  );

  const addReaction = async (messageId: number, reaction: string) => {
    const existingMsg = await findGroupMsgRecord(messageId, group_token);

    if (existingMsg) {
      const messageReactions = existingMsg.reactions ? JSON.parse(existingMsg.reactions) : null;
      const updatedReactions: Reaction[] = [
        ...(Array.isArray(messageReactions)
          ? messageReactions?.filter((r: Reaction) => r.uid !== +currentUserId)
          : []),
        {
          datetime: new Date().toISOString(),
          reaction: reaction,
          uid: +currentUserId
        }
      ];

      const newReactions = JSON.stringify(updatedReactions);
      await database.write(async () => {
        existingMsg.update((r) => {
          r.reactions = newReactions;
          addMessageDirtyAction(r, {
            type: 'reaction',
            value: reaction as string
          });
        });
      });

      setIsModalVisible(false);
      await triggerMessagePush(token, sendWsEvent);
    }

    const message = giftedMessages.find((msg) => msg._id === messageId);
    sendWebSocketMessage('new_reaction', message, reaction);
  };

  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;
    }

    let replyText = replaceMentionsWithNames(currentMessage.replyMessage.text);

    const replyMsg = giftedMessages.find((m) => m._id === currentMessage.replyMessage?.id);

    if (replyMsg && replyMsg?.attachment && replyMsg?.attachment?.filename) {
      replyText = replyMsg.attachment.filename;
    }

    if (replyMsg && replyMsg.image) {
      replyText = '🏞️photo';
    }

    if (replyMsg && replyMsg.video) {
      replyText = '📽️video';
    }

    if (replyMsg && replyMsg.attachment?.filetype === 'nomadmania/location') {
      replyText = '📍location';
    }

    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
              }
            ]}
          >
            {replyText}
          </Text>
        </View>
      </TouchableOpacity>
    );
  };

  const scrollToMessage = (messageId: number) => {
    if (!giftedMessages) return;

    const messageIndex = giftedMessages.findIndex((message) => message._id === messageId);

    if (messageIndex !== -1 && flatList.current) {
      setIsSearchingMessage(null);

      flatList.current.scrollToIndex({
        index: messageIndex,
        animated: true,
        viewPosition: 0.5
      });

      setHighlightedMessageId(messageId);
    }

    if (hasMoreMessages && messageIndex === -1) {
      setIsSearchingMessage(messageId);
      loadEarlierMessages();
    }
  };

  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 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) => {
    if (canSendMessages === 0) return null;

    return (
      <>
        {showMentions && canSeeMembers ? (
          <MentionsList
            mentionList={mentionList}
            inputHeight={inputHeight}
            onMentionSelect={onMentionSelect}
          />
        ) : null}
        <View
          onLayout={(e) => {
            setInputHeight(e.nativeEvent.layout.height);
          }}
        >
          <InputToolbar
            {...props}
            renderActions={() =>
              userType === 'normal' && !editingMessage ? (
                <Actions
                  icon={() => (
                    <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />
                  )}
                  onPressActionButton={openAttachmentsModal}
                />
              ) : null
            }
            containerStyle={{
              backgroundColor: Colors.FILL_LIGHT
            }}
          />
        </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;
  };

  const onInputTextChanged = (value: string) => {
    handleTyping(value.length > 0);

    setText(value);

    const mentionMatch = value.match(/(^|\s)(@\w*)$/);

    if (mentionMatch) {
      setShowMentions(true);
      const searchText = mentionMatch[2].slice(1).toLowerCase();
      setMentionList(
        (storedMembers ?? [])?.filter(
          (m: any) => m.name.toLowerCase().includes(searchText) && m.uid !== +currentUserId
        )
      );
    } else {
      setShowMentions(false);
    }
  };

  const onMentionSelect = (member: { uid: number; name: string }) => {
    const words = text.split(' ');
    words[words.length - 1] = `@${member.name} `;
    setText(words.join(' '));
    setShowMentions(false);
  };

  const transformMessageForServer = (text: string) => {
    let transformedText = text;
    storedMembers?.forEach((member: any) => {
      const mentionRegex = new RegExp(`@${member.name}\\b`, 'g');
      transformedText = transformedText.replace(mentionRegex, `@{${member.uid}}`);
    });
    return transformedText;
  };

  const renderComposer = useCallback((props: ComposerProps) => {
    return (
      // <Composer {...props} textInputStyle={styles.composer} />
      <View style={{ flex: 1, paddingHorizontal: 10, paddingTop: 6 }}>
        <TextInput
          ref={textInputRef}
          multiline
          textAlignVertical="center"
          placeholder=""
          value={props.text}
          onChangeText={props.onTextChanged}
          style={styles.composer}
          selectionColor={Colors.LIGHT_GRAY}
        />
      </View>
    );
  }, []);

  return (
    <SafeAreaView
      edges={['top']}
      style={{
        height: '100%'
      }}
    >
      <View style={{ paddingHorizontal: '5%' }}>
        <Header
          label={groupName}
          textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
          rightElement={
            <TouchableOpacity
              onPress={() =>
                navigation.navigate(
                  ...([NAVIGATION_PAGES.GROUP_SETTINGS, { groupToken: group_token }] as never)
                )
              }
              disabled={userType !== 'normal' || announcement === 1}
            >
              {groupAvatar && userType === 'normal' ? (
                <Image
                  source={{ uri: `${groupAvatar}?cacheBust=${cacheKey}` }}
                  style={styles.avatar}
                />
              ) : userType === 'normal' ? (
                <GroupIcon fill={Colors.DARK_BLUE} width={30} height={30} />
              ) : (
                <BanIcon fill={Colors.RED} width={30} height={30} />
              )}
            </TouchableOpacity>
          }
        />
      </View>
      {pinned && (
        <TouchableOpacity
          style={{
            height: 38,
            flexDirection: 'row',
            backgroundColor: Colors.FILL_LIGHT,
            borderBottomWidth: 1,
            borderBottomColor: Colors.DARK_LIGHT
          }}
          onPress={() => scrollToMessage(pinned.id)}
        >
          <View
            style={{
              height: 50,
              width: 6,
              backgroundColor: Colors.DARK_BLUE
            }}
          ></View>
          <View
            style={{
              paddingLeft: 8,
              height: '100%',
              justifyContent: 'center'
            }}
          >
            <PinIcon fill={Colors.DARK_BLUE} height={18} />
          </View>

          <View style={{ flex: 1, justifyContent: 'center' }}>
            <Text style={{ color: Colors.DARK_BLUE, paddingLeft: 10 }} numberOfLines={1}>
              {pinned.text}
            </Text>
          </View>

          {data?.settings?.admin === 1 && (
            <View style={{ alignItems: 'flex-end', justifyContent: 'center' }}>
              <TouchableOpacity
                style={{ paddingRight: 10 }}
                onPress={() => handlePinMessage(pinned.id, 0)}
              >
                <MaterialCommunityIcons
                  name="close-circle-outline"
                  size={24}
                  color={Colors.DARK_BLUE}
                />
              </TouchableOpacity>
            </View>
          )}
        </TouchableOpacity>
      )}

      <GestureHandlerRootView style={styles.container}>
        {giftedMessagesWithUnread &&
        ((canSeeMembers && storedMembers) ||
          (data && data.settings && !canSeeMembers) ||
          !isConnected) ? (
          <GiftedChat
            messages={giftedMessagesWithUnread as CustomMessage[]}
            text={text}
            listViewProps={{
              ref: flatList,
              showsVerticalScrollIndicator: false,
              initialNumToRender: 50,
              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={(props) => (
              <RenderMessageImage
                props={props}
                token={token}
                currentUserId={+currentUserId}
                onLongPress={handleLongPress}
                setSelectedMedia={setSelectedMedia}
              />
            )}
            showUserAvatar={true}
            onPressAvatar={(user) => {
              navigation.navigate(
                ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: user._id }] as never)
              );
            }}
            renderInputToolbar={renderInputToolbar}
            renderCustomView={renderReplyMessageView}
            isCustomViewBottom={false}
            messageContainerRef={messageContainerRef as any}
            minComposerHeight={34}
            onInputTextChanged={onInputTextChanged}
            textInputRef={textInputRef as any}
            isTyping={isTyping ? true : false}
            renderTypingIndicator={() => <TypingIndicator isTyping={isTyping} />}
            renderSend={(props) =>
              editingMessage ? (
                <View style={[styles.sendBtn, { paddingHorizontal: 8 }]}>
                  {props.text?.trim() && (
                    <Send
                      {...props}
                      containerStyle={{
                        justifyContent: 'center'
                      }}
                    >
                      <View style={styles.editBtn}>
                        <MaterialCommunityIcons name="check" size={22} color={Colors.WHITE} />
                      </View>
                    </Send>
                  )}
                  {!props.text?.trim() && (
                    <View style={[styles.editBtn, { backgroundColor: Colors.LIGHT_GRAY }]}>
                      <MaterialCommunityIcons name="check" size={22} color={Colors.WHITE} />
                    </View>
                  )}
                </View>
              ) : (
                <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}
                clearEditMessage={clearEditMessage}
                message={
                  replyMessage
                    ? { ...replyMessage, text: replaceMentionsWithNames(replyMessage.text) }
                    : null
                }
                editingMessage={editingMessage}
              />
            )}
            maxComposerHeight={120}
            renderComposer={renderComposer}
            keyboardShouldPersistTaps="handled"
            renderChatEmpty={() => (
              <View style={styles.emptyChat}>
                <Text
                  style={styles.emptyChatText}
                >{`No messages yet.\nFeel free to start the conversation.`}</Text>
              </View>
            )}
            shouldUpdateMessage={shouldUpdateMessage}
            isScrollToBottomEnabled={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');
                }
              },
              {
                pattern: /@\{(\d+)\}/g,
                renderText: (messageText: string) => {
                  const tagId = messageText.slice(2, messageText.length - 1);
                  const user = (storedMembers ?? [])?.find((m: any) => m.uid === +tagId);

                  if (user) {
                    return (
                      <Text
                        style={{ color: Colors.ORANGE }}
                        onPress={() =>
                          navigation.navigate(
                            ...([
                              NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
                              { userId: user.uid }
                            ] as never)
                          )
                        }
                      >
                        @{user.name}
                      </Text>
                    );
                  } else {
                    return messageText;
                  }
                }
              }
            ]}
            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} />
        )}

        <CustomImageViewer
          images={[
            {
              uri: selectedMedia,
              cache: 'force-cache',
              headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
            }
          ]}
          imageIndex={0}
          visible={!!selectedMedia}
          onRequestClose={() => setSelectedMedia(null)}
          backgroundColor={Colors.DARK_BLUE}
          withSaveButton={true}
          onRequestSave={() => downloadImageFromViewer(selectedMedia, token)}
        />

        <ReactModal
          isVisible={isModalVisible}
          onBackdropPress={handleBackgroundPress}
          onBackButtonPress={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()}
              <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 />
        <GroupStatusModal />
      </GestureHandlerRootView>
      {!isKeyboardVisible ? (
        <View
          style={{
            height: insets.bottom,
            backgroundColor: insetsColor
          }}
        />
      ) : null}
    </SafeAreaView>
  );
};

export default GroupChatScreen;
