123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314 |
- import React, { useState, useCallback, useEffect, useRef } from 'react';
- import {
- View,
- TouchableOpacity,
- Image,
- Modal,
- Text,
- FlatList,
- Dimensions,
- Alert,
- ScrollView,
- Linking,
- Platform,
- ActivityIndicator
- } from 'react-native';
- import {
- GiftedChat,
- Bubble,
- InputToolbar,
- Actions,
- IMessage,
- Send,
- BubbleProps,
- Composer,
- TimeProps,
- MessageProps
- } from 'react-native-gifted-chat';
- import { MaterialCommunityIcons } from '@expo/vector-icons';
- import * as ImagePicker from 'expo-image-picker';
- import { useActionSheet } from '@expo/react-native-action-sheet';
- import {
- GestureHandlerRootView,
- LongPressGestureHandler,
- 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 { Video } from 'expo-av';
- import ChatMessageBox from '../Components/ChatMessageBox';
- import ReplyMessageBar from '../Components/ReplyMessageBar';
- import { useSharedValue, withTiming } from 'react-native-reanimated';
- import { BlurView } from 'expo-blur';
- import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
- import Clipboard from '@react-native-clipboard/clipboard';
- import { trigger } from 'react-native-haptic-feedback';
- import ReactModal from 'react-native-modal';
- import { storage, StoreType } from 'src/storage';
- import {
- usePostDeleteMessageMutation,
- usePostGetChatWithQuery,
- usePostMessagesReadMutation,
- usePostReactToMessageMutation,
- usePostSendMessageMutation
- } from '@api/chat';
- import { CustomMessage, Message, Reaction } from '../types';
- import { API_HOST, WEBSOCKET_URL } from 'src/constants';
- import { getFontSize } from 'src/utils';
- import ReactionBar from '../Components/ReactionBar';
- import OptionsMenu from '../Components/OptionsMenu';
- import EmojiSelectorModal from '../Components/EmojiSelectorModal';
- import { styles } from './styles';
- import SendIcon from 'assets/icons/messages/send.svg';
- import { SheetManager } from 'react-native-actions-sheet';
- import { NAVIGATION_PAGES } from 'src/types';
- import * as Notifications from 'expo-notifications';
- import { usePushNotification } from 'src/contexts/PushNotificationContext';
- import ReactionsListModal from '../Components/ReactionsListModal';
- const options = {
- enableVibrateFallback: true,
- ignoreAndroidSystemSettings: false
- };
- const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
- const ChatScreen = ({ route }: { route: any }) => {
- const { id, name, avatar }: { id: number; name: string; avatar: string | null } = route.params;
- const currentUserId = storage.get('uid', StoreType.STRING) as number;
- const token = storage.get('token', StoreType.STRING) as string;
- const insets = useSafeAreaInsets();
- const [messages, setMessages] = useState<CustomMessage[] | null>();
- const { showActionSheetWithOptions } = useActionSheet();
- const navigation = useNavigation();
- const { data: chatData, isFetching, refetch } = usePostGetChatWithQuery(token, id, -1, -1, true);
- const { mutateAsync: sendMessage } = usePostSendMessageMutation();
- const swipeableRowRef = useRef<Swipeable | null>(null);
- const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
- const [selectedMedia, setSelectedMedia] = useState<any>(null);
- const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
- const [modalInfo, setModalInfo] = useState({
- visible: false,
- type: 'confirm',
- message: '',
- action: () => {}
- });
- const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | null>(null);
- const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false);
- const [messagePosition, setMessagePosition] = useState<{
- x: number;
- y: number;
- width: number;
- height: number;
- isMine: boolean;
- } | null>(null);
- const [isModalVisible, setIsModalVisible] = useState(false);
- const [unreadMessageIndex, setUnreadMessageIndex] = useState<number | null>(null);
- const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
- const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
- const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
- const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
- const [isRerendering, setIsRerendering] = useState<boolean>(false);
- const [isTyping, setIsTyping] = useState<boolean>(false);
- const messageRefs = useRef<{ [key: string]: any }>({});
- const flatList = useRef<FlatList | null>(null);
- const scrollY = useSharedValue(0);
- const { isSubscribed } = usePushNotification();
- const socket = useRef<WebSocket | null>(null);
- const closeModal = () => {
- setModalInfo({ ...modalInfo, visible: false });
- };
- const dismissChatNotifications = async (chatWithUserId: number) => {
- const { status } = await Notifications.getPermissionsAsync();
- if (status !== 'granted' || !isSubscribed) {
- setModalInfo({
- visible: true,
- type: 'success',
- message:
- 'To use this feature we need your permission to access your notifications. You will be redirected to the notification settings screen where you need to enable them.',
- action: () =>
- // @ts-ignore
- navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
- screen: NAVIGATION_PAGES.NOTIFICATIONS
- })
- });
- return;
- }
- const getNotificationData = (notification: Notifications.Notification) => {
- if (Platform.OS === 'android') {
- const data = notification.request.content.data;
- if (data?.params) {
- try {
- return JSON.parse(data.params) ?? {};
- } catch (error) {
- console.error('Error parsing params:', error);
- return {};
- }
- } else {
- Notifications.dismissNotificationAsync(notification.request.identifier);
- return {};
- }
- } else {
- const data = (notification.request.trigger as Notifications.PushNotificationTrigger)
- ?.payload;
- if (data?.params) {
- try {
- return JSON.parse(data.params as string) ?? {};
- } catch (error) {
- console.error('Error parsing params:', error);
- return {};
- }
- }
- }
- };
- const clearNotificationsFromUser = async (userId: number) => {
- const presentedNotifications = await Notifications.getPresentedNotificationsAsync();
- presentedNotifications.forEach((notification) => {
- const parsedParams = getNotificationData(notification);
- const conversation_with_user = parsedParams?.id;
- if (conversation_with_user === userId) {
- Notifications.dismissNotificationAsync(notification.request.identifier);
- }
- });
- };
- await clearNotificationsFromUser(chatWithUserId);
- Notifications.setNotificationHandler({
- handleNotification: async (notification) => {
- let conversation_with_user = 0;
- const parsedParams = getNotificationData(notification);
- conversation_with_user = parsedParams?.id;
- if (conversation_with_user === chatWithUserId) {
- return {
- shouldShowAlert: false,
- shouldPlaySound: false,
- shouldSetBadge: false
- };
- }
- return {
- shouldShowAlert: true,
- shouldPlaySound: false,
- shouldSetBadge: false
- };
- }
- });
- return () => {
- Notifications.setNotificationHandler({
- handleNotification: async () => ({
- shouldShowAlert: true,
- shouldPlaySound: false,
- shouldSetBadge: false
- })
- });
- };
- };
- useEffect(() => {
- let unsubscribe: any;
- const setupNotificationHandler = async () => {
- unsubscribe = await dismissChatNotifications(id);
- };
- setupNotificationHandler();
- return () => {
- if (unsubscribe) unsubscribe();
- };
- }, [id]);
- useEffect(() => {
- socket.current = new WebSocket(WEBSOCKET_URL);
- socket.current.onopen = () => {
- socket.current?.send(JSON.stringify({ token }));
- };
- socket.current.onmessage = (event) => {
- const data = JSON.parse(event.data);
- handleWebSocketMessage(data);
- };
- socket.current.onclose = () => {
- console.log('WebSocket connection closed chat screen');
- };
- return () => {
- socket.current?.close();
- };
- }, [token]);
- const handleWebSocketMessage = (data: any) => {
- switch (data.action) {
- case 'new_message':
- if (data.conversation_with === id) {
- refetch();
- }
- break;
- case 'is_typing':
- if (data.conversation_with === id) {
- setIsTyping(true);
- }
- break;
- case 'stopped_typing':
- if (data.conversation_with === id) {
- setIsTyping(false);
- }
- break;
- case 'new_reaction':
- if (data.conversation_with === id) {
- refetch();
- }
- break;
- default:
- break;
- }
- };
- const sendWebSocketMessage = (action: string) => {
- if (socket.current && socket.current.readyState === WebSocket.OPEN) {
- socket.current.send(JSON.stringify({ action, conversation_with: id }));
- }
- };
- const handleTyping = (isTyping: boolean) => {
- if (isTyping) {
- sendWebSocketMessage('is_typing');
- } else {
- sendWebSocketMessage('stopped_typing');
- }
- };
- const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
- return {
- _id: message.id,
- text: message.text,
- createdAt: new Date(message.sent_datetime + 'Z'),
- user: {
- _id: message.sender,
- name: message.sender === id ? name : 'Me'
- },
- replyMessage:
- message.reply_to_id !== -1
- ? {
- text: message.reply_to.text,
- id: message.reply_to.id,
- name: message.reply_to.sender === id ? 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
- };
- };
- useFocusEffect(
- useCallback(() => {
- if (chatData?.messages) {
- const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
- if (unreadMessageIndex === null && Platform.OS === 'ios') {
- const firstUnreadIndex = mappedMessages.findLastIndex(
- (msg) => !msg.received && !msg?.deleted && msg.user._id === id
- );
- if (firstUnreadIndex !== -1) {
- setUnreadMessageIndex(firstUnreadIndex);
- const unreadMarker: any = {
- _id: 'unreadMarker',
- text: 'Unread messages',
- system: true
- };
- mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker);
- } else {
- setUnreadMessageIndex(0);
- }
- }
- setMessages(mappedMessages);
- }
- }, [chatData])
- );
- const sentToServer = useRef<Set<number>>(new Set());
- const handleViewableItemsChanged = ({ viewableItems }: { viewableItems: any[] }) => {
- const newViewableUnreadMessages = viewableItems
- .filter(
- (item) =>
- !item.item.received &&
- !item.item.deleted &&
- !item.item.system &&
- item.item.user._id === id &&
- !sentToServer.current.has(item.item._id)
- )
- .map((item) => item.item._id);
- if (newViewableUnreadMessages.length > 0) {
- markMessagesAsRead(
- {
- token,
- from_user: id,
- messages_id: newViewableUnreadMessages
- },
- {
- onSuccess: (res) => {
- newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
- // sendWebSocketMessage('messages_read');
- sendWebSocketMessage('new_message');
- }
- }
- );
- }
- };
- const renderSystemMessage = (props: any) => {
- if (props.currentMessage._id === 'unreadMarker') {
- return (
- <View style={styles.unreadMessagesContainer}>
- <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
- </View>
- );
- }
- return null;
- };
- const clearReplyMessage = () => setReplyMessage(null);
- const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
- const messageRef = messageRefs.current[message._id];
- setSelectedMessage(props);
- trigger('impactMedium', options);
- const isMine = message.user._id === +currentUserId;
- if (messageRef) {
- messageRef.measureInWindow((x: number, y: number, width: number, height: number) => {
- const screenHeight = Dimensions.get('window').height;
- const spaceAbove = y - insets.top;
- const spaceBelow = screenHeight - (y + height) - insets.bottom * 2;
- let finalY = y;
- scrollY.value = 0;
- if (isNaN(y) || isNaN(height)) {
- console.error("Invalid measurement values for 'y' or 'height'", { y, height });
- return;
- }
- if (spaceBelow < 160) {
- const extraShift = 160 - spaceBelow;
- finalY -= extraShift;
- }
- if (spaceAbove < 50) {
- const extraShift = 50 - spaceAbove;
- finalY += extraShift;
- }
- if (spaceBelow < 160 || spaceAbove < 50) {
- const targetY = screenHeight / 2 - height / 2;
- scrollY.value = withTiming(finalY - finalY);
- }
- if (height > Dimensions.get('window').height - 200) {
- finalY = 100;
- }
- finalY = isNaN(finalY) ? 0 : finalY;
- setMessagePosition({ x, y: finalY, width, height, isMine });
- setIsModalVisible(true);
- });
- }
- };
- const openEmojiSelector = () => {
- SheetManager.show('emoji-selector');
- trigger('impactLight', options);
- };
- const closeEmojiSelector = () => {
- SheetManager.hide('emoji-selector');
- };
- const handleReactionPress = (emoji: string, messageId: number) => {
- addReaction(messageId, emoji);
- };
- const handleDeleteMessage = (messageId: number) => {
- deleteMessage(
- {
- token,
- message_id: messageId,
- conversation_with_user: id
- },
- {
- onSuccess: () => {
- setMessages((prevMessages) =>
- prevMessages ? prevMessages.filter((msg) => msg._id !== messageId) : []
- );
- // sendWebSocketMessage('message_deleted');
- refetch();
- sendWebSocketMessage('new_message');
- }
- }
- );
- };
- const handleOptionPress = (option: string) => {
- if (!selectedMessage) return;
- switch (option) {
- case 'reply':
- setReplyMessage(selectedMessage.currentMessage);
- setIsModalVisible(false);
- break;
- case 'copy':
- Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
- setIsModalVisible(false);
- Alert.alert('Copied');
- break;
- case 'delete':
- handleDeleteMessage(selectedMessage.currentMessage?._id);
- setIsModalVisible(false);
- break;
- default:
- break;
- }
- closeEmojiSelector();
- };
- const openReactionList = (
- reactions: { uid: number; name: string; reaction: string }[],
- messageId: number
- ) => {
- SheetManager.show('reactions-list-modal', {
- payload: {
- users: reactions,
- currentUserId: +currentUserId,
- token,
- messageId,
- conversation_with_user: id,
- setMessages,
- sendWebSocketMessage
- } as any
- });
- };
- const renderTimeContainer = (time: TimeProps<CustomMessage>) => {
- const createdAt = new Date(time.currentMessage.createdAt);
- const formattedTime = createdAt.toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit',
- hour12: true
- });
- const hasReactions =
- time.currentMessage.reactions &&
- Array.isArray(time.currentMessage.reactions) &&
- time.currentMessage.reactions.length > 0;
- return (
- <View
- style={{
- flexDirection: 'row',
- justifyContent: hasReactions ? 'space-between' : 'flex-end',
- alignItems: 'center',
- paddingHorizontal: 8,
- paddingBottom: 6,
- flexShrink: 1,
- flexGrow: 1,
- gap: 12
- }}
- >
- {hasReactions && (
- <TouchableOpacity
- style={[
- {
- flexDirection: 'row',
- alignItems: 'center',
- flexShrink: 0,
- backgroundColor:
- time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)',
- borderRadius: 12,
- paddingHorizontal: 6,
- paddingVertical: 4,
- gap: 6
- }
- ]}
- onPress={() =>
- Array.isArray(time.currentMessage.reactions) &&
- openReactionList(
- time.currentMessage.reactions.map((reaction) => ({
- ...reaction,
- name: reaction.uid === id ? 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={{
- flexDirection: 'row',
- gap: 4,
- alignItems: 'center',
- alignSelf: 'flex-end'
- }}
- >
- <Text
- style={{
- color: Colors.LIGHT_GRAY,
- fontSize: getFontSize(10),
- fontWeight: '600',
- paddingLeft: 8,
- flexShrink: 0
- }}
- >
- {formattedTime}
- </Text>
- {renderTicks(time.currentMessage)}
- </View>
- </View>
- );
- };
- const renderSelectedMessage = () =>
- selectedMessage && (
- <View
- style={{
- maxHeight: '80%',
- width: messagePosition?.width,
- position: 'absolute',
- top: messagePosition?.y,
- left: messagePosition?.x
- }}
- >
- <ScrollView>
- <Bubble
- {...selectedMessage}
- wrapperStyle={{
- right: { backgroundColor: Colors.DARK_BLUE },
- left: { backgroundColor: Colors.FILL_LIGHT }
- }}
- textStyle={{
- right: { color: Colors.WHITE },
- left: { color: Colors.DARK_BLUE }
- }}
- renderTicks={() => null}
- renderTime={renderTimeContainer}
- />
- </ScrollView>
- </View>
- );
- const handleBackgroundPress = () => {
- setIsModalVisible(false);
- setSelectedMessage(null);
- closeEmojiSelector();
- };
- useFocusEffect(
- useCallback(() => {
- navigation?.getParent()?.setOptions({
- tabBarStyle: {
- display: 'none'
- }
- });
- }, [navigation])
- );
- const onSend = useCallback(
- (newMessages: CustomMessage[] = []) => {
- if (replyMessage) {
- newMessages[0].replyMessage = {
- text: replyMessage.text,
- id: replyMessage._id,
- name: replyMessage.user._id === id ? name : 'Me'
- };
- }
- const message = { ...newMessages[0], pending: true };
- sendMessage(
- {
- token,
- to_uid: id,
- text: message.text,
- reply_to_id: replyMessage ? (replyMessage._id as number) : -1
- },
- {
- onSuccess: () => sendWebSocketMessage('new_message'),
- onError: (err) => console.log('err', err)
- }
- );
- setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
- clearReplyMessage();
- },
- [replyMessage]
- );
- const openActionSheet = () => {
- const options = ['Open Camera', 'Select from gallery', 'Cancel'];
- const cancelButtonIndex = 2;
- showActionSheetWithOptions(
- {
- options,
- cancelButtonIndex
- },
- async (buttonIndex) => {
- if (buttonIndex === 0) {
- openCamera();
- } else if (buttonIndex === 1) {
- openGallery();
- }
- }
- );
- };
- const openCamera = async () => {
- const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
- if (permissionResult.granted === false) {
- alert('Permission denied to access camera');
- return;
- }
- const result = await ImagePicker.launchCameraAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.All,
- quality: 1,
- allowsEditing: true
- });
- if (!result.canceled) {
- const newMedia = {
- _id: Date.now().toString(),
- createdAt: new Date(),
- user: { _id: +currentUserId, name: 'Me' },
- image: result.assets[0].type === 'image' ? result.assets[0].uri : null,
- video: result.assets[0].type === 'video' ? result.assets[0].uri : null
- };
- setMessages((previousMessages) =>
- GiftedChat.append(previousMessages ?? [], [newMedia as any])
- );
- }
- };
- const openGallery = async () => {
- const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
- if (permissionResult.granted === false) {
- alert('Denied');
- return;
- }
- const result = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.All,
- allowsMultipleSelection: true,
- quality: 1
- });
- if (!result.canceled && result.assets) {
- const imageMessages = result.assets.map((asset) => ({
- _id: Date.now().toString() + asset.uri,
- createdAt: new Date(),
- user: { _id: +currentUserId, name: 'Me' },
- image: asset.type === 'image' ? asset.uri : null,
- video: asset.type === 'video' ? asset.uri : null
- }));
- setMessages((previousMessages) =>
- GiftedChat.append(previousMessages ?? [], imageMessages as any[])
- );
- }
- };
- const renderMessageVideo = (props: BubbleProps<CustomMessage>) => {
- const { currentMessage } = props;
- if (currentMessage.video) {
- return (
- <LongPressGestureHandler
- onHandlerStateChange={(event) => handleLongPress(currentMessage, props)}
- >
- <TouchableOpacity
- onPress={() => setSelectedMedia(currentMessage.video)}
- style={styles.mediaContainer}
- >
- <Video
- source={{ uri: currentMessage.video }}
- style={styles.chatMedia}
- useNativeControls
- />
- </TouchableOpacity>
- </LongPressGestureHandler>
- );
- }
- return null;
- };
- const addReaction = (messageId: number, reaction: string) => {
- if (!messages) return;
- const updatedMessages = messages.map((msg: any) => {
- if (msg._id === messageId) {
- const updatedReactions: Reaction[] = [
- ...(Array.isArray(msg.reactions)
- ? msg.reactions?.filter((r: Reaction) => r.uid !== +currentUserId)
- : []),
- { datetime: new Date().toISOString(), reaction: reaction, uid: +currentUserId }
- ];
- return {
- ...msg,
- reactions: updatedReactions
- };
- }
- return msg;
- });
- setMessages(updatedMessages);
- reactToMessage(
- { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
- {
- onSuccess: () => sendWebSocketMessage('new_reaction'),
- onError: (err) => console.log('err', err)
- }
- );
- setIsModalVisible(false);
- };
- const updateRowRef = useCallback(
- (ref: any) => {
- if (
- ref &&
- replyMessage &&
- ref.props.children.props.currentMessage?._id === replyMessage._id
- ) {
- swipeableRowRef.current = ref;
- }
- },
- [replyMessage]
- );
- const renderReplyMessageView = (props: BubbleProps<CustomMessage>) => {
- if (!props.currentMessage) {
- return null;
- }
- const { currentMessage } = props;
- if (!currentMessage || !currentMessage?.replyMessage) {
- return null;
- }
- return (
- <TouchableOpacity
- style={[
- styles.replyMessageContainer,
- {
- backgroundColor:
- currentMessage.user._id === id ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.2)',
- borderColor: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE
- }
- ]}
- onPress={() => {
- if (currentMessage?.replyMessage?.id) {
- scrollToMessage(currentMessage.replyMessage.id);
- }
- }}
- >
- <View style={styles.replyContent}>
- <Text
- style={[
- styles.replyAuthorName,
- { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
- ]}
- >
- {currentMessage.replyMessage.name}
- </Text>
- <Text
- numberOfLines={1}
- style={[
- styles.replyMessageText,
- { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
- ]}
- >
- {currentMessage.replyMessage.text}
- </Text>
- </View>
- </TouchableOpacity>
- );
- };
- const scrollToMessage = (messageId: number) => {
- if (!messages) return;
- const messageIndex = messages.findIndex((message) => message._id === messageId);
- if (messageIndex !== -1 && flatList.current) {
- flatList.current.scrollToIndex({
- index: messageIndex,
- animated: true,
- viewPosition: 0.5
- });
- setHighlightedMessageId(messageId);
- }
- };
- useEffect(() => {
- if (highlightedMessageId && isRerendering) {
- setTimeout(() => {
- setHighlightedMessageId(null);
- setIsRerendering(false);
- }, 1500);
- }
- }, [highlightedMessageId, isRerendering]);
- useEffect(() => {
- if (replyMessage && swipeableRowRef.current) {
- swipeableRowRef.current.close();
- swipeableRowRef.current = null;
- }
- }, [replyMessage]);
- const renderMessageImage = (props: any) => {
- const { currentMessage } = props;
- return (
- <TouchableOpacity
- onPress={() => setSelectedMedia(currentMessage.image)}
- style={styles.imageContainer}
- >
- <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
- </TouchableOpacity>
- );
- };
- const renderTicks = (message: CustomMessage) => {
- if (message.user._id === id) return null;
- return message.received ? (
- <View>
- <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
- </View>
- ) : message.sent ? (
- <View>
- <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
- </View>
- ) : message.pending ? (
- <View>
- <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
- </View>
- ) : null;
- };
- const renderBubble = (props: BubbleProps<CustomMessage>) => {
- const { currentMessage } = props;
- if (currentMessage.deleted) {
- const text = currentMessage.text.length
- ? props.currentMessage.text
- : 'This message was deleted';
- return (
- <View>
- <Bubble
- {...props}
- renderTime={() => null}
- currentMessage={{
- ...props.currentMessage,
- text: text
- }}
- renderMessageText={() => (
- <View style={{ paddingHorizontal: 12, paddingVertical: 6 }}>
- <Text style={{ color: Colors.LIGHT_GRAY, fontStyle: 'italic', fontSize: 12 }}>
- {text}
- </Text>
- </View>
- )}
- wrapperStyle={{
- right: {
- backgroundColor: Colors.DARK_BLUE
- },
- left: {
- backgroundColor: Colors.FILL_LIGHT
- }
- }}
- textStyle={{
- left: {
- color: Colors.DARK_BLUE
- },
- right: {
- color: Colors.WHITE
- }
- }}
- />
- </View>
- );
- }
- const isHighlighted = currentMessage._id === highlightedMessageId;
- const backgroundColor = isHighlighted
- ? Colors.ORANGE
- : currentMessage.user._id === +currentUserId
- ? Colors.DARK_BLUE
- : Colors.FILL_LIGHT;
- return (
- <View
- key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
- ref={(ref) => {
- if (ref && currentMessage) {
- messageRefs.current[currentMessage._id] = ref;
- }
- }}
- collapsable={false}
- >
- <Bubble
- {...props}
- wrapperStyle={{
- right: {
- backgroundColor: backgroundColor
- },
- left: {
- backgroundColor: backgroundColor
- }
- }}
- textStyle={{
- left: {
- color: Colors.DARK_BLUE
- },
- right: {
- color: Colors.FILL_LIGHT
- }
- }}
- onLongPress={() => handleLongPress(currentMessage, props)}
- renderTicks={() => null}
- renderTime={renderTimeContainer}
- />
- </View>
- );
- };
- const renderInputToolbar = (props: any) => (
- <InputToolbar
- {...props}
- renderActions={() =>
- // <Actions
- // icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
- // // onPressActionButton={openActionSheet}
- // />
- null
- }
- containerStyle={{
- backgroundColor: Colors.FILL_LIGHT
- }}
- />
- );
- const renderScrollToBottom = () => {
- return (
- <TouchableOpacity
- style={{
- position: 'absolute',
- bottom: -20,
- right: -20,
- backgroundColor: Colors.DARK_BLUE,
- borderRadius: 20,
- padding: 8
- }}
- 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={name}
- rightElement={
- <TouchableOpacity
- onPress={() =>
- navigation.navigate(
- ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
- )
- }
- >
- {avatar ? (
- <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
- ) : (
- <AvatarWithInitials
- text={
- name
- .split(/ (.+)/)
- .map((n) => n[0])
- .join('') ?? ''
- }
- flag={API_HOST + 'flag.png'}
- size={30}
- fontSize={12}
- />
- )}
- </TouchableOpacity>
- }
- />
- </View>
- <GestureHandlerRootView style={styles.container}>
- {messages ? (
- <GiftedChat
- messages={messages as CustomMessage[]}
- listViewProps={{
- ref: flatList,
- showsVerticalScrollIndicator: false,
- initialNumToRender: 30,
- onViewableItemsChanged: handleViewableItemsChanged,
- viewabilityConfig: { itemVisiblePercentThreshold: 50 },
- initialScrollIndex: unreadMessageIndex ?? 0,
- onScrollToIndexFailed: (info: any) => {
- const wait = new Promise((resolve) => setTimeout(resolve, 300));
- wait.then(() => {
- flatList.current?.scrollToIndex({
- index: info.index,
- animated: true,
- viewPosition: 0.5
- });
- });
- }
- }}
- renderSystemMessage={renderSystemMessage}
- onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
- user={{ _id: +currentUserId, name: 'Me' }}
- renderBubble={renderBubble}
- renderMessageImage={renderMessageImage}
- renderInputToolbar={renderInputToolbar}
- renderCustomView={renderReplyMessageView}
- isCustomViewBottom={false}
- messageContainerRef={messageContainerRef}
- minComposerHeight={34}
- onInputTextChanged={(text) => handleTyping(text.length > 0)}
- isTyping={isTyping}
- renderSend={(props) => (
- <View
- style={{
- flexDirection: 'row',
- height: '100%',
- alignItems: 'center',
- justifyContent: 'center',
- paddingHorizontal: 14
- }}
- >
- {props.text?.trim() && (
- <Send
- {...props}
- containerStyle={{
- justifyContent: 'center'
- }}
- >
- <SendIcon fill={Colors.DARK_BLUE} />
- </Send>
- )}
- {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
- </View>
- )}
- 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} />
- )}
- // renderMessageVideo={renderMessageVideo}
- renderAvatar={null}
- maxComposerHeight={100}
- renderComposer={(props) => <Composer {...props} />}
- keyboardShouldPersistTaps="handled"
- renderChatEmpty={() => (
- <View style={styles.emptyChat}>
- <Text
- style={styles.emptyChatText}
- >{`No messages yet.\nFeel free to start the conversation.`}</Text>
- </View>
- )}
- shouldUpdateMessage={shouldUpdateMessage}
- scrollToBottom={true}
- scrollToBottomComponent={renderScrollToBottom}
- scrollToBottomStyle={{ backgroundColor: 'transparent' }}
- parsePatterns={(linkStyle) => [
- {
- type: 'url',
- style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
- onPress: (url: string) => Linking.openURL(url),
- onLongPress: (url: string) => {
- Clipboard.setString(url ?? '');
- Alert.alert('Link copied');
- }
- }
- ]}
- />
- ) : (
- <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
- )}
- <Modal visible={!!selectedMedia} transparent={true}>
- <View style={styles.modalContainer}>
- {selectedMedia && selectedMedia?.includes('.mp4') ? (
- <Video
- source={{ uri: selectedMedia }}
- style={styles.fullScreenMedia}
- useNativeControls
- />
- ) : (
- <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
- )}
- <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
- <MaterialCommunityIcons name="close" size={30} color="white" />
- </TouchableOpacity>
- </View>
- </Modal>
- <ReactModal
- isVisible={isModalVisible}
- onBackdropPress={handleBackgroundPress}
- style={styles.reactModalContainer}
- animationIn="fadeIn"
- animationOut="fadeOut"
- useNativeDriver
- backdropColor="transparent"
- >
- <BlurView
- intensity={80}
- style={styles.modalBackground}
- experimentalBlurMethod="dimezisBlurView"
- >
- <TouchableOpacity
- style={styles.modalBackground}
- activeOpacity={1}
- onPress={handleBackgroundPress}
- >
- <ReactionBar
- messagePosition={messagePosition}
- selectedMessage={selectedMessage}
- reactionEmojis={reactionEmojis}
- handleReactionPress={handleReactionPress}
- openEmojiSelector={openEmojiSelector}
- />
- {renderSelectedMessage()}
- <OptionsMenu
- selectedMessage={selectedMessage}
- handleOptionPress={handleOptionPress}
- messagePosition={messagePosition}
- />
- <EmojiSelectorModal
- visible={emojiSelectorVisible}
- selectedMessage={selectedMessage}
- addReaction={addReaction}
- closeEmojiSelector={closeEmojiSelector}
- />
- </TouchableOpacity>
- </BlurView>
- </ReactModal>
- <WarningModal
- isVisible={modalInfo.visible}
- onClose={closeModal}
- type={modalInfo.type}
- message={modalInfo.message}
- action={() => {
- modalInfo.action();
- closeModal();
- }}
- />
- <ReactionsListModal />
- </GestureHandlerRootView>
- <View
- style={{
- height: insets.bottom,
- backgroundColor: Colors.FILL_LIGHT
- }}
- />
- </SafeAreaView>
- );
- };
- export default ChatScreen;
|