Browse Source

edit message

Viktoriia 3 months ago
parent
commit
101e2dd8ef

+ 18 - 1
src/modules/api/chat/chat-api.ts

@@ -198,6 +198,20 @@ export interface PostDeleteGroupMessage {
   group_token: string;
 }
 
+export interface PostEditMessage {
+  token: string;
+  message_id: number;
+  to_uid: number;
+  text: string;
+}
+
+export interface PostEditGroupMessage {
+  token: string;
+  message_id: number;
+  group_token: string;
+  text: string;
+}
+
 export interface PostDeleteChat {
   token: string;
   conversation_with_user: number;
@@ -499,5 +513,8 @@ export const chatApi = {
       message_id,
       group_token,
       pin
-    })
+    }),
+  editGroupMessage: (data: PostEditGroupMessage) =>
+    request.postForm<ResponseType>(API.EDIT_GROUP_MESSAGE, data),
+  editMessage: (data: PostEditMessage) => request.postForm<ResponseType>(API.EDIT_MESSAGE, data)
 };

+ 3 - 1
src/modules/api/chat/chat-query-keys.tsx

@@ -53,5 +53,7 @@ export const chatQueryKeys = {
   canCreateGroup: (token: string) => ['canCreateGroup', token] as const,
   getPinnedGroup: (token: string, group_token: string) =>
     ['getPinnedGroup', token, group_token] as const,
-  setPinGroupMessage: () => ['setPinGroupMessage'] as const
+  setPinGroupMessage: () => ['setPinGroupMessage'] as const,
+  editGroupMessage: () => ['editGroupMessage'] as const,
+  editMessage: () => ['editMessage'] as const
 };

+ 2 - 0
src/modules/api/chat/queries/index.ts

@@ -39,3 +39,5 @@ export * from './use-post-remove-group-chat-from-conversation-list';
 export * from './use-post-can-create-group';
 export * from './use-post-get-pinned-group-message';
 export * from './use-post-set-pin-group-message';
+export * from './use-post-edit-group-message';
+export * from './use-post-edit-message';

+ 17 - 0
src/modules/api/chat/queries/use-post-edit-group-message.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostEditGroupMessage } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostEditGroupMessageMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostEditGroupMessage, ResponseType>({
+    mutationKey: chatQueryKeys.editGroupMessage(),
+    mutationFn: async (data) => {
+      const response = await chatApi.editGroupMessage(data);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/chat/queries/use-post-edit-message.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostEditMessage } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostEditMessageMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostEditMessage, ResponseType>({
+    mutationKey: chatQueryKeys.editMessage(),
+    mutationFn: async (data) => {
+      const response = await chatApi.editMessage(data);
+      return response.data;
+    }
+  });
+};

+ 149 - 26
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -47,7 +47,8 @@ import {
   usePostGetChatWithQuery,
   usePostMessagesReadMutation,
   usePostReactToMessageMutation,
-  usePostSendMessageMutation
+  usePostSendMessageMutation,
+  usePostEditMessageMutation
 } from '@api/chat';
 import { CustomMessage, Message, Reaction } from '../types';
 import { API_HOST, APP_VERSION, WEBSOCKET_URL } from 'src/constants';
@@ -60,7 +61,7 @@ import { SheetManager } from 'react-native-actions-sheet';
 import { NAVIGATION_PAGES } from 'src/types';
 import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
-import { dismissChatNotifications } from '../utils';
+import { dismissChatNotifications, isMessageEdited } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import FileViewer from 'react-native-file-viewer';
 import * as FileSystem from 'expo-file-system';
@@ -141,6 +142,7 @@ const ChatScreen = ({ route }: { route: any }) => {
   const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
   const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
   const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
+  const { mutateAsync: editMessage } = usePostEditMessageMutation();
 
   const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
   const [isRerendering, setIsRerendering] = useState<boolean>(false);
@@ -154,6 +156,9 @@ const ChatScreen = ({ route }: { route: any }) => {
   const [hasMoreMessages, setHasMoreMessages] = useState(true);
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
+  const [text, setText] = useState('');
+  const [editingMessage, setEditingMessage] = useState<CustomMessage | null>(null);
+
   const appState = useRef(AppState.currentState);
   const textInputRef = useRef<TextInput>(null);
 
@@ -192,6 +197,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           },
           pending: true,
           isSending: true,
+          edited: false,
           image: file.type === 'image' ? file.uri : undefined,
           video: file.type === 'video' ? file.uri : undefined
         };
@@ -262,6 +268,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         user: { _id: +currentUserId, name: 'Me' },
         pending: true,
         deleted: false,
+        edited: false,
         reactions: {},
         attachment: {
           id: -1,
@@ -341,6 +348,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           deleted: false,
           reactions: {},
           isSending: true,
+          edited: false,
           attachment: {
             id: -1,
             filename: file.name ?? 'File',
@@ -567,7 +575,7 @@ const ChatScreen = ({ route }: { route: any }) => {
       user: { _id: +currentUserId, name: 'Me' },
       system: false
     };
-    setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
+    // setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
   }, []);
 
   useEffect(() => {
@@ -717,6 +725,20 @@ const ChatScreen = ({ route }: { route: any }) => {
         }
         break;
 
+      case 'edited_message':
+        if (data.conversation_with === id && data.message) {
+          setMessages(
+            (prevMessages) =>
+              prevMessages?.map((msg) => {
+                if (msg._id === data.message.id) {
+                  return { ...msg, text: data.message.text, edited: true };
+                }
+                return msg;
+              }) ?? []
+          );
+        }
+        break;
+
       default:
         break;
     }
@@ -847,6 +869,13 @@ const ChatScreen = ({ route }: { route: any }) => {
         data.messages_ids = readMessagesIds;
       }
 
+      if (action === 'edited_message' && message) {
+        data.message = {
+          id: message._id,
+          text: message.text
+        };
+      }
+
       socket.current.send(JSON.stringify(data));
     }
   };
@@ -882,6 +911,7 @@ const ChatScreen = ({ route }: { route: any }) => {
       sent: message.status === 2,
       received: message.status === 3,
       deleted: message.status === 4,
+      edited: isMessageEdited(message.edits),
       isSending: false,
       video:
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
@@ -1020,6 +1050,17 @@ const ChatScreen = ({ route }: { route: any }) => {
 
   const clearReplyMessage = () => setReplyMessage(null);
 
+  const clearEditMessage = () => {
+    setEditingMessage(null);
+    setText('');
+  };
+
+  const onInputTextChanged = (value: string) => {
+    handleTyping(value.length > 0);
+
+    setText(value);
+  };
+
   const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
     const messageRef = messageRefs.current[message._id];
 
@@ -1042,8 +1083,8 @@ const ChatScreen = ({ route }: { route: any }) => {
           return;
         }
 
-        if (spaceBelow < 160) {
-          const extraShift = 160 - spaceBelow;
+        if (spaceBelow < 180) {
+          const extraShift = 180 - spaceBelow;
           finalY -= extraShift;
         }
 
@@ -1052,7 +1093,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           finalY += extraShift;
         }
 
-        if (spaceBelow < 160 || spaceAbove < 50) {
+        if (spaceBelow < 180 || spaceAbove < 50) {
           const targetY = screenHeight / 2 - height / 2;
           scrollY.value = withTiming(finalY - finalY);
         }
@@ -1140,6 +1181,10 @@ const ChatScreen = ({ route }: { route: any }) => {
         downloadFileToDevice(selectedMessage.currentMessage);
         setIsModalVisible(false);
         break;
+      case 'edit':
+        handleEditMessage(selectedMessage.currentMessage);
+        setIsModalVisible(false);
+        break;
       default:
         break;
     }
@@ -1233,7 +1278,10 @@ const ChatScreen = ({ route }: { route: any }) => {
           </TouchableOpacity>
         )}
         <View style={styles.timeContainer}>
-          <Text style={styles.timeText}>{formattedTime}</Text>
+          {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>
@@ -1294,8 +1342,55 @@ const ChatScreen = ({ route }: { route: any }) => {
     }, [navigation])
   );
 
+  const handleEditMessage = (message: CustomMessage) => {
+    setReplyMessage(null);
+    setEditingMessage(message);
+    setText(message.text);
+    textInputRef.current?.focus();
+  };
+
   const onSend = useCallback(
     (newMessages: CustomMessage[] = []) => {
+      if (editingMessage) {
+        if (editingMessage.text !== newMessages[0].text) {
+          setMessages((prevMessages) =>
+            (prevMessages ?? []).map((msg) =>
+              msg._id === editingMessage._id ? { ...msg, text: newMessages[0].text } : msg
+            )
+          );
+
+          editMessage(
+            {
+              token,
+              to_uid: id,
+              message_id: editingMessage._id,
+              text: newMessages[0].text
+            },
+            {
+              onSuccess: (res) => {
+                const editedMessage = {
+                  _id: editingMessage._id,
+                  text: newMessages[0].text
+                };
+
+                setMessages((previousMessages) =>
+                  (previousMessages ?? []).map((msg) =>
+                    msg._id === editingMessage._id
+                      ? { ...msg, isSending: false, edited: true }
+                      : msg
+                  )
+                );
+                sendWebSocketMessage('edited_message', editedMessage as unknown as CustomMessage);
+              }
+            }
+          );
+        }
+
+        clearEditMessage();
+        clearReplyMessage();
+        return;
+      }
+
       if (replyMessage) {
         newMessages[0].replyMessage = {
           text: replyMessage.text,
@@ -1334,7 +1429,7 @@ const ChatScreen = ({ route }: { route: any }) => {
 
       clearReplyMessage();
     },
-    [replyMessage]
+    [replyMessage, editingMessage]
   );
 
   const addReaction = (messageId: number, reaction: string) => {
@@ -1685,7 +1780,7 @@ const ChatScreen = ({ route }: { route: any }) => {
     <InputToolbar
       {...props}
       renderActions={() =>
-        userType === 'normal' ? (
+        userType === 'normal' && !editingMessage ? (
           <Actions
             icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
             onPressActionButton={openAttachmentsModal}
@@ -1768,6 +1863,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         {messages ? (
           <GiftedChat
             messages={messages as CustomMessage[]}
+            text={text}
             listViewProps={{
               ref: flatList,
               showsVerticalScrollIndicator: false,
@@ -1795,24 +1891,46 @@ const ChatScreen = ({ route }: { route: any }) => {
             isCustomViewBottom={false}
             messageContainerRef={messageContainerRef}
             minComposerHeight={34}
-            onInputTextChanged={(text) => handleTyping(text.length > 0)}
+            onInputTextChanged={onInputTextChanged}
             textInputRef={textInputRef}
             isTyping={isTyping}
-            renderSend={(props) => (
-              <View style={styles.sendBtn}>
-                {props.text?.trim() && (
-                  <Send
-                    {...props}
-                    containerStyle={{
-                      justifyContent: 'center'
-                    }}
-                  >
-                    <SendIcon fill={Colors.DARK_BLUE} />
-                  </Send>
-                )}
-                {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
-              </View>
-            )}
+            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}
@@ -1834,7 +1952,12 @@ const ChatScreen = ({ route }: { route: any }) => {
               />
             )}
             renderChatFooter={() => (
-              <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+              <ReplyMessageBar
+                clearReply={clearReplyMessage}
+                clearEditMessage={clearEditMessage}
+                message={replyMessage}
+                editingMessage={editingMessage}
+              />
             )}
             renderAvatar={null}
             maxComposerHeight={100}

+ 8 - 0
src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx

@@ -121,6 +121,14 @@ export const styles = StyleSheet.create({
     justifyContent: 'center',
     paddingHorizontal: 14
   },
+  editBtn: {
+    backgroundColor: Colors.DARK_BLUE,
+    width: 32,
+    height: 32,
+    borderRadius: 16,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
   timeContainer: {
     flexDirection: 'row',
     gap: 4,

+ 8 - 1
src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx

@@ -77,10 +77,17 @@ const OptionsMenu: React.FC<OptionsMenuProps> = ({
       ) : null}
       {isGroup && isAdmin ? (
         <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('pin')}>
-          <Text style={styles.optionText}>Pin message</Text>
+          <Text style={styles.optionText}>Pin</Text>
           <PinIcon height={16} fill={Colors.DARK_BLUE} />
         </TouchableOpacity>
       ) : null}
+
+      {messagePosition.isMine ? (
+        <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('edit')}>
+          <Text style={styles.optionText}>Edit</Text>
+          <MaterialCommunityIcons name="pencil" size={20} color={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      ) : null}
     </Animated.View>
   );
 

+ 23 - 8
src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx

@@ -7,18 +7,26 @@ import { CustomMessage } from '../types';
 
 type ReplyMessageBarProps = {
   clearReply: () => void;
+  clearEditMessage: () => void;
   message: CustomMessage | null;
+  editingMessage: CustomMessage | null;
 };
 
 const replyMessageBarHeight = 50;
 
-const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
-  if (!message) return null;
+const ReplyMessageBar = ({
+  clearReply,
+  clearEditMessage,
+  message,
+  editingMessage
+}: ReplyMessageBarProps) => {
+  if (!message && !editingMessage) return null;
 
-  const text =
-    message?.attachment && message?.attachment?.filename
+  const text = editingMessage
+    ? editingMessage.text
+    : message?.attachment && message?.attachment?.filename
       ? message.attachment.filename
-      : message.text;
+      : message?.text;
 
   return (
     <Animated.View
@@ -38,7 +46,11 @@ const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
         }}
       ></View>
       <View style={styles.replyImageContainer}>
-        <MaterialCommunityIcons name="reply" size={20} color={Colors.DARK_BLUE} />
+        <MaterialCommunityIcons
+          name={editingMessage ? 'pencil' : 'reply'}
+          size={20}
+          color={Colors.DARK_BLUE}
+        />
       </View>
 
       <View style={{ flex: 1 }}>
@@ -51,7 +63,7 @@ const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
             fontSize: 15
           }}
         >
-          {message.user.name}
+          {editingMessage ? 'Edit message' : message?.user.name}
         </Text>
         <Text style={{ color: Colors.DARK_BLUE, paddingLeft: 10, paddingTop: 5 }} numberOfLines={1}>
           {text}
@@ -59,7 +71,10 @@ const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
       </View>
 
       <View style={{ alignItems: 'flex-end', justifyContent: 'center' }}>
-        <TouchableOpacity style={styles.crossButton} onPress={clearReply}>
+        <TouchableOpacity
+          style={styles.crossButton}
+          onPress={editingMessage ? clearEditMessage : clearReply}
+        >
           <MaterialCommunityIcons name="close-circle-outline" size={24} color={Colors.DARK_BLUE} />
         </TouchableOpacity>
       </View>

+ 150 - 21
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -55,7 +55,8 @@ import {
   usePostGetPinnedGroupMessageQuery,
   usePostSetPinGroupMessageMutation,
   usePostGetGroupSettingsQuery,
-  usePostGetGroupMembersQuery
+  usePostGetGroupMembersQuery,
+  usePostEditGroupMessageMutation
 } from '@api/chat';
 import { CustomMessage, GroupMessage, Reaction } from '../types';
 import { API_HOST, APP_VERSION, WEBSOCKET_URL } from 'src/constants';
@@ -69,7 +70,7 @@ import { SheetManager } from 'react-native-actions-sheet';
 import { NAVIGATION_PAGES } from 'src/types';
 import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
-import { dismissChatNotifications } from '../utils';
+import { dismissChatNotifications, isMessageEdited } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import FileViewer from 'react-native-file-viewer';
 import * as FileSystem from 'expo-file-system';
@@ -173,6 +174,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   const { mutateAsync: deleteMessage } = usePostDeleteGroupMessageMutation();
   const { mutateAsync: reactToMessage } = usePostReactToGroupMessageMutation();
   const { mutateAsync: pinMessage } = usePostSetPinGroupMessageMutation();
+  const { mutateAsync: editMessage } = usePostEditGroupMessageMutation();
 
   const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
   const [isRerendering, setIsRerendering] = useState<boolean>(false);
@@ -191,6 +193,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   const [mentionList, setMentionList] = useState<any>([]);
   const [showMentions, setShowMentions] = useState(false);
   const [inputHeight, setInputHeight] = useState(45);
+  const [editingMessage, setEditingMessage] = useState<CustomMessage | null>(null);
 
   const appState = useRef(AppState.currentState);
   const textInputRef = useRef<TextInput>(null);
@@ -236,6 +239,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           },
           pending: true,
           isSending: true,
+          edited: false,
           image: file.type === 'image' ? file.uri : undefined,
           video: file.type === 'video' ? file.uri : undefined
         };
@@ -307,6 +311,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         user: { _id: +currentUserId, name: 'Me', avatar: null as never },
         pending: true,
         deleted: false,
+        edited: false,
         reactions: {},
         attachment: {
           id: -1,
@@ -386,6 +391,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           deleted: false,
           reactions: {},
           isSending: true,
+          edited: false,
           attachment: {
             id: -1,
             filename: file.name ?? 'File',
@@ -775,6 +781,20 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         }
         break;
 
+      case 'edited_message':
+        if (data.group_token === group_token && data.message) {
+          setMessages(
+            (prevMessages) =>
+              prevMessages?.map((msg) => {
+                if (msg._id === data.message.id) {
+                  return { ...msg, text: data.message.text, edited: true };
+                }
+                return msg;
+              }) ?? []
+          );
+        }
+        break;
+
       default:
         break;
     }
@@ -907,6 +927,13 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         data.messages_ids = readMessagesIds;
       }
 
+      if (action === 'edited_message' && message) {
+        data.message = {
+          id: message._id,
+          text: message.text
+        };
+      }
+
       socket.current.send(JSON.stringify(data));
     }
   };
@@ -949,6 +976,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
       sent: message.status === 2,
       received: message.status === 3,
       deleted: message.status === 4,
+      edited: isMessageEdited(message.edits),
       isSending: false,
       video:
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
@@ -1129,6 +1157,11 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
   const clearReplyMessage = () => setReplyMessage(null);
 
+  const clearEditMessage = () => {
+    setEditingMessage(null);
+    setText('');
+  };
+
   const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
     const messageRef = messageRefs.current[message._id];
 
@@ -1283,6 +1316,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         handlePinMessage(selectedMessage.currentMessage?._id, 1);
         setIsModalVisible(false);
         break;
+      case 'edit':
+        handleEditMessage(selectedMessage.currentMessage);
+        setIsModalVisible(false);
+        break;
       default:
         break;
     }
@@ -1378,7 +1415,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           </TouchableOpacity>
         )}
         <View style={styles.timeContainer}>
-          <Text style={styles.timeText}>{formattedTime}</Text>
+          {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>
@@ -1439,8 +1479,66 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     }, [navigation])
   );
 
+  const replaceMentionsWithNames = (text: string) => {
+    const userList = members?.settings ?? [];
+
+    return text.replace(/@\{(\d+)\}/g, (_, uid) => {
+      const user = userList.find((m) => 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(
     (newMessages: CustomMessage[] = []) => {
+      if (editingMessage) {
+        if (editingMessage.text !== newMessages[0].text) {
+          const editedText = transformMessageForServer(newMessages[0].text);
+          setMessages((prevMessages) =>
+            (prevMessages ?? []).map((msg) =>
+              msg._id === editingMessage._id ? { ...msg, text: editedText, isSending: true } : msg
+            )
+          );
+
+          editMessage(
+            {
+              token,
+              group_token: group_token,
+              message_id: editingMessage._id,
+              text: editedText
+            },
+            {
+              onSuccess: () => {
+                const editedMessage = {
+                  _id: editingMessage._id,
+                  text: editedText
+                };
+
+                setMessages((previousMessages) =>
+                  (previousMessages ?? []).map((msg) =>
+                    msg._id === editingMessage._id
+                      ? { ...msg, isSending: false, edited: true }
+                      : msg
+                  )
+                );
+                sendWebSocketMessage('edited_message', editedMessage as unknown as CustomMessage);
+              }
+            }
+          );
+        }
+
+        clearEditMessage();
+        clearReplyMessage();
+        return;
+      }
+
       if (replyMessage) {
         newMessages[0].replyMessage = {
           text: transformMessageForServer(replyMessage.text),
@@ -1488,7 +1586,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
       clearReplyMessage();
     },
-    [replyMessage]
+    [replyMessage, editingMessage]
   );
 
   const addReaction = (messageId: number, reaction: string) => {
@@ -1908,7 +2006,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           <InputToolbar
             {...props}
             renderActions={() =>
-              userType === 'normal' ? (
+              userType === 'normal' && !editingMessage ? (
                 <Actions
                   icon={() => (
                     <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />
@@ -2115,21 +2213,43 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             textInputRef={textInputRef}
             isTyping={isTyping ? true : false}
             renderTypingIndicator={() => <TypingIndicator isTyping={isTyping} />}
-            renderSend={(props) => (
-              <View style={styles.sendBtn}>
-                {props.text?.trim() && (
-                  <Send
-                    {...props}
-                    containerStyle={{
-                      justifyContent: 'center'
-                    }}
-                  >
-                    <SendIcon fill={Colors.DARK_BLUE} />
-                  </Send>
-                )}
-                {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
-              </View>
-            )}
+            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}
@@ -2151,7 +2271,16 @@ const GroupChatScreen = ({ route }: { route: any }) => {
               />
             )}
             renderChatFooter={() => (
-              <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+              <ReplyMessageBar
+                clearReply={clearReplyMessage}
+                clearEditMessage={clearEditMessage}
+                message={
+                  replyMessage
+                    ? { ...replyMessage, text: replaceMentionsWithNames(replyMessage.text) }
+                    : null
+                }
+                editingMessage={editingMessage}
+              />
             )}
             maxComposerHeight={100}
             renderComposer={(props) => <Composer {...props} />}

+ 1 - 0
src/screens/InAppScreens/MessagesScreen/types.ts

@@ -104,6 +104,7 @@ export interface CustomMessage extends IMessage {
   deleted: boolean;
   attachment: Attachement | null;
   reactions: Reaction[] | {};
+  edited: boolean;
   image?: string;
   video?: string;
   isSending?: boolean;

+ 9 - 0
src/screens/InAppScreens/MessagesScreen/utils.ts

@@ -114,3 +114,12 @@ export const dismissChatNotifications = async (
     });
   };
 };
+
+export const isMessageEdited = (edits: string) => {
+  try {
+    const parsedEdits = JSON.parse(edits);
+    return Array.isArray(parsedEdits) && parsedEdits.length > 0;
+  } catch (error) {
+    return false;
+  }
+};

+ 6 - 2
src/types/api.ts

@@ -182,7 +182,9 @@ export enum API_ENDPOINT {
   REMOVE_GROUP_FROM_LIST = 'remove-group-chat-from-conversation-list',
   CAN_CREATE_GROUP = 'can-create-group',
   GET_PINNED_GROUP_MESSAGE = 'get-pinned-group-message',
-  SET_PIN_GROUP_MESSAGE = 'set-pin-group-message'
+  SET_PIN_GROUP_MESSAGE = 'set-pin-group-message',
+  EDIT_GROUP_MESSAGE = 'edit-group-message',
+  EDIT_MESSAGE = 'edit-message'
 }
 
 export enum API {
@@ -339,7 +341,9 @@ export enum API {
   REMOVE_GROUP_FROM_LIST = `${API_ROUTE.CHAT}/${API_ENDPOINT.REMOVE_GROUP_FROM_LIST}`,
   CAN_CREATE_GROUP = `${API_ROUTE.CHAT}/${API_ENDPOINT.CAN_CREATE_GROUP}`,
   GET_PINNED_GROUP_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_PINNED_GROUP_MESSAGE}`,
-  SET_PIN_GROUP_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_PIN_GROUP_MESSAGE}`
+  SET_PIN_GROUP_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_PIN_GROUP_MESSAGE}`,
+  EDIT_GROUP_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.EDIT_GROUP_MESSAGE}`,
+  EDIT_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.EDIT_MESSAGE}`
 }
 
 export type BaseAxiosError = AxiosError;