Viktoriia преди 4 месеца
родител
ревизия
47ce60f7c1

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

@@ -274,6 +274,16 @@ export interface PostGetGroupMessageStatusReturn {
   }[];
 }
 
+export interface PostGetPinnedGroupMessageReturn extends ResponseType {
+  message: GroupMessage;
+  pinned_by: {
+    uid: number;
+    name: string;
+    avatar: string | null;
+  };
+  pinned_time: string;
+}
+
 export const chatApi = {
   searchUsers: (token: string, search: string) =>
     request.postForm<PostSearchUsersReturn>(API.SEARCH_USERS, { token, search }),
@@ -477,5 +487,17 @@ export const chatApi = {
   removeGroupFromList: (token: string, group_token: string) =>
     request.postForm<ResponseType>(API.REMOVE_GROUP_FROM_LIST, { token, group_token }),
   canCreateGroup: (token: string) =>
-    request.postForm<PostCanCreateGroupReturn>(API.CAN_CREATE_GROUP, { token })
+    request.postForm<PostCanCreateGroupReturn>(API.CAN_CREATE_GROUP, { token }),
+  getPinnedGroup: (token: string, group_token: string) =>
+    request.postForm<PostGetPinnedGroupMessageReturn>(API.GET_PINNED_GROUP_MESSAGE, {
+      token,
+      group_token
+    }),
+  setPinGroupMessage: (token: string, message_id: number, group_token: string, pin: 0 | 1) =>
+    request.postForm<ResponseType>(API.SET_PIN_GROUP_MESSAGE, {
+      token,
+      message_id,
+      group_token,
+      pin
+    })
 };

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

@@ -50,5 +50,8 @@ export const chatQueryKeys = {
   getGroupMessageStatus: (token: string, group_token: string, message_id: number) =>
     ['getGroupMessageStatus', token, group_token, message_id] as const,
   removeGroupFromList: () => ['removeGroupFromList'] as const,
-  canCreateGroup: (token: string) => ['canCreateGroup', token] as const
+  canCreateGroup: (token: string) => ['canCreateGroup', token] as const,
+  getPinnedGroup: (token: string, group_token: string) =>
+    ['getPinnedGroup', token, group_token] as const,
+  setPinGroupMessage: () => ['setPinGroupMessage'] as const
 };

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

@@ -37,3 +37,5 @@ export * from './use-post-update-group-settings';
 export * from './use-post-get-group-message-status';
 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';

+ 21 - 0
src/modules/api/chat/queries/use-post-get-pinned-group-message.tsx

@@ -0,0 +1,21 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostGetPinnedGroupMessageReturn } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetPinnedGroupMessageQuery = (
+  token: string,
+  group_token: string,
+  enabled: boolean
+) => {
+  return useQuery<PostGetPinnedGroupMessageReturn, BaseAxiosError>({
+    queryKey: chatQueryKeys.getPinnedGroup(token, group_token),
+    queryFn: async () => {
+      const response = await chatApi.getPinnedGroup(token, group_token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 27 - 0
src/modules/api/chat/queries/use-post-set-pin-group-message.tsx

@@ -0,0 +1,27 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostSetPinGroupMessageMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; message_id: number; group_token: string; pin: 0 | 1 },
+    ResponseType
+  >({
+    mutationKey: chatQueryKeys.setPinGroupMessage(),
+    mutationFn: async (data) => {
+      const response = await chatApi.setPinGroupMessage(
+        data.token,
+        data.message_id,
+        data.group_token,
+        data.pin
+      );
+      return response.data;
+    }
+  });
+};

+ 1 - 1
src/screens/InAppScreens/MessagesScreen/Components/EditGroupModal.tsx

@@ -91,7 +91,7 @@ const SearchModal = () => {
           <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
         ) : (
           <AvatarWithInitials
-            text={item.name?.split(' ')[0][0] + item.name?.split(' ')[1][0]}
+            text={item.name?.split(' ')[0][0] + (item.name?.split(' ')[1][0] ?? '')}
             flag={API_HOST + item?.flag1}
             size={36}
             fontSize={16}

+ 1 - 1
src/screens/InAppScreens/MessagesScreen/Components/GroupStatusModal.tsx

@@ -78,7 +78,7 @@ const GroupStatusModal = () => {
           <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
         ) : (
           <AvatarWithInitials
-            text={item.name?.split(' ')[0][0] + item.name?.split(' ')[1][0]}
+            text={item.name?.split(' ')[0][0] + (item.name?.split(' ')[1][0] ?? '')}
             flag={API_HOST + item?.flag1}
             size={36}
             fontSize={16}

+ 10 - 5
src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx

@@ -3,6 +3,7 @@ import { TouchableOpacity, Text, StyleSheet, Dimensions } from 'react-native';
 import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import { Colors } from 'src/theme';
+import PinIcon from 'assets/icons/messages/pin.svg';
 
 interface MessagePosition {
   x: number;
@@ -17,13 +18,15 @@ interface OptionsMenuProps {
   selectedMessage: any;
   handleOptionPress: (option: string) => void;
   isGroup?: boolean;
+  isAdmin?: boolean;
 }
 
 const OptionsMenu: React.FC<OptionsMenuProps> = ({
   messagePosition,
   selectedMessage,
   handleOptionPress,
-  isGroup
+  isGroup,
+  isAdmin
 }) =>
   selectedMessage &&
   messagePosition && (
@@ -72,10 +75,12 @@ const OptionsMenu: React.FC<OptionsMenuProps> = ({
           <MaterialCommunityIcons name="information-outline" size={20} color={Colors.DARK_BLUE} />
         </TouchableOpacity>
       ) : null}
-      {/* <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('pin')}>
-        <Text style={styles.optionText}>Pin message</Text>
-        <MaterialCommunityIcons name="pin" size={20} color={Colors.DARK_BLUE} />
-      </TouchableOpacity> */}
+      {isGroup && isAdmin ? (
+        <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('pin')}>
+          <Text style={styles.optionText}>Pin message</Text>
+          <PinIcon height={16} fill={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      ) : null}
     </Animated.View>
   );
 

+ 131 - 6
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -49,7 +49,10 @@ import {
   usePostSendGroupMessageMutation,
   usePostReactToGroupMessageMutation,
   usePostGroupMessagesReadMutation,
-  usePostDeleteGroupMessageMutation
+  usePostDeleteGroupMessageMutation,
+  usePostGetPinnedGroupMessageQuery,
+  usePostSetPinGroupMessageMutation,
+  usePostGetGroupSettingsQuery
 } from '@api/chat';
 import { CustomMessage, GroupMessage, Reaction } from '../types';
 import { API_HOST, WEBSOCKET_URL } from 'src/constants';
@@ -77,6 +80,7 @@ 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';
 
 const options = {
   enableVibrateFallback: true,
@@ -116,12 +120,22 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     refetch: refetch,
     isFetching: isFetching
   } = usePostGetGroupChatQuery(token, group_token, 50, prevThenMessageId, true);
+  const { data: pinData, refetch: refetchPinned } = usePostGetPinnedGroupMessageQuery(
+    token,
+    group_token,
+    true
+  );
+  const { data } = usePostGetGroupSettingsQuery(token, group_token, true);
+
   const { mutateAsync: sendMessage } = usePostSendGroupMessageMutation();
 
+  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,
@@ -147,6 +161,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   const { mutateAsync: markMessagesAsRead } = usePostGroupMessagesReadMutation();
   const { mutateAsync: deleteMessage } = usePostDeleteGroupMessageMutation();
   const { mutateAsync: reactToMessage } = usePostReactToGroupMessageMutation();
+  const { mutateAsync: pinMessage } = usePostSetPinGroupMessageMutation();
 
   const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
   const [isRerendering, setIsRerendering] = useState<boolean>(false);
@@ -180,6 +195,12 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     });
   }, []);
 
+  useEffect(() => {
+    if (pinData && pinData?.message) {
+      setPinned(pinData.message);
+    }
+  }, [pinData]);
+
   const onSendMedia = useCallback(
     async (files: { uri: string; type: 'image' | 'video' }[]) => {
       for (const file of files) {
@@ -985,6 +1006,19 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     }, [chatData])
   );
 
+  useEffect(() => {
+    if (messages) {
+      if (isSearchingMessage) {
+        const messageIndex = messages.findIndex((msg) => msg._id === isSearchingMessage);
+
+        if (messageIndex !== -1 && flatList.current) {
+          setIsSearchingMessage(null);
+        }
+        scrollToMessage(isSearchingMessage);
+      }
+    }
+  }, [messages]);
+
   useEffect(() => {
     if (messages?.length === 0 && !modalInfo.visible) {
       setTimeout(() => {
@@ -994,7 +1028,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   }, [modalInfo]);
 
   const loadEarlierMessages = async () => {
-    if (!hasMoreMessages || isLoadingEarlier || !messages) return;
+    if (!hasMoreMessages || (isLoadingEarlier && !isSearchingMessage) || !messages) return;
 
     setIsLoadingEarlier(true);
 
@@ -1093,8 +1127,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           return;
         }
 
-        if (spaceBelow < 160) {
-          const extraShift = 160 - spaceBelow;
+        if (spaceBelow < 220) {
+          const extraShift = 220 - spaceBelow;
           finalY -= extraShift;
         }
 
@@ -1103,7 +1137,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           finalY += extraShift;
         }
 
-        if (spaceBelow < 160 || spaceAbove < 50) {
+        if (spaceBelow < 220 || spaceAbove < 50) {
           const targetY = screenHeight / 2 - height / 2;
           scrollY.value = withTiming(finalY - finalY);
         }
@@ -1170,6 +1204,25 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     );
   };
 
+  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;
 
@@ -1202,6 +1255,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         setIsModalVisible(false);
         setInsetsColor(Colors.WHITE);
         break;
+      case 'pin':
+        handlePinMessage(selectedMessage.currentMessage?._id, 1);
+        setIsModalVisible(false);
+        break;
       default:
         break;
     }
@@ -1519,6 +1576,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     const messageIndex = messages.findIndex((message) => message._id === messageId);
 
     if (messageIndex !== -1 && flatList.current) {
+      setIsSearchingMessage(null);
+
       flatList.current.scrollToIndex({
         index: messageIndex,
         animated: true,
@@ -1526,6 +1585,21 @@ const GroupChatScreen = ({ route }: { route: any }) => {
       });
 
       setHighlightedMessageId(messageId);
+      setMessages((previousMessages) =>
+        (previousMessages ?? []).map((msg) =>
+          msg._id === messageId
+            ? {
+                ...msg,
+                isRendering: msg?.isRendering ? false : true
+              }
+            : msg
+        )
+      );
+    }
+
+    if (hasMoreMessages && messageIndex === -1) {
+      setIsSearchingMessage(messageId);
+      loadEarlierMessages();
     }
   };
 
@@ -1854,6 +1928,56 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           }
         />
       </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}>
         {messages ? (
@@ -1862,7 +1986,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             listViewProps={{
               ref: flatList,
               showsVerticalScrollIndicator: false,
-              initialNumToRender: 30,
+              initialNumToRender: messages.length,
               onViewableItemsChanged: handleViewableItemsChanged,
               viewabilityConfig: { itemVisiblePercentThreshold: 50 },
               onScrollToIndexFailed: (info: any) => {
@@ -2013,6 +2137,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
                 handleOptionPress={handleOptionPress}
                 messagePosition={messagePosition}
                 isGroup={true}
+                isAdmin={data?.settings?.admin == 1}
               />
               <EmojiSelectorModal
                 visible={emojiSelectorVisible}

+ 1 - 1
src/screens/InAppScreens/MessagesScreen/GroupSettingsScreen/index.tsx

@@ -256,7 +256,7 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
                       <Image source={{ uri: API_HOST + member.avatar }} style={styles.avatar} />
                     ) : (
                       <AvatarWithInitials
-                        text={`${member.name?.split(' ')[0][0]}${member.name?.split(' ')[1][0]}`}
+                        text={`${member.name?.split(' ')[0][0]}${member.name?.split(' ')[1][0] ?? ''}`}
                         flag={API_HOST + member?.flag}
                         size={36}
                         fontSize={16}

+ 1 - 1
src/screens/InAppScreens/MessagesScreen/MembersListScreen/index.tsx

@@ -77,7 +77,7 @@ const MembersListScreen: FC<Props> = ({ navigation, route }) => {
         <Image source={{ uri: API_HOST + item.avatar }} style={chatStyles.avatar} />
       ) : (
         <AvatarWithInitials
-          text={item.name?.split(' ')[0][0] + item.name?.split(' ')[1][0]}
+          text={item.name?.split(' ')[0][0] + (item.name?.split(' ')[1][0] ?? '')}
           flag={API_HOST + item?.flag1}
           size={36}
           fontSize={12}

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

@@ -107,6 +107,7 @@ export interface CustomMessage extends IMessage {
   image?: string;
   video?: string;
   isSending?: boolean;
+  isRendering?: boolean;
 }
 
 export type Reaction = {

+ 6 - 2
src/types/api.ts

@@ -181,7 +181,9 @@ export enum API_ENDPOINT {
   UPDATE_GROUP_SETTINGS = 'update-group-settings',
   GET_GROUP_MESSAGE_STATUS = 'get-group-message-status',
   REMOVE_GROUP_FROM_LIST = 'remove-group-chat-from-conversation-list',
-  CAN_CREATE_GROUP = 'can-create-group'
+  CAN_CREATE_GROUP = 'can-create-group',
+  GET_PINNED_GROUP_MESSAGE = 'get-pinned-group-message',
+  SET_PIN_GROUP_MESSAGE = 'set-pin-group-message'
 }
 
 export enum API {
@@ -337,7 +339,9 @@ export enum API {
   UPDATE_GROUP_SETTINGS = `${API_ROUTE.CHAT}/${API_ENDPOINT.UPDATE_GROUP_SETTINGS}`,
   GET_GROUP_MESSAGE_STATUS = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_GROUP_MESSAGE_STATUS}`,
   REMOVE_GROUP_FROM_LIST = `${API_ROUTE.CHAT}/${API_ENDPOINT.REMOVE_GROUP_FROM_LIST}`,
-  CAN_CREATE_GROUP = `${API_ROUTE.CHAT}/${API_ENDPOINT.CAN_CREATE_GROUP}`
+  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}`
 }
 
 export type BaseAxiosError = AxiosError;