Ver Fonte

websockets, edit group settings, TypingIndicator, fixes

Viktoriia há 4 meses atrás
pai
commit
5f14cf184d

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

@@ -45,6 +45,9 @@ export interface PostGetGroupSettingsReturn extends ResponseType {
     description: string | null;
     avatar: string | null;
     avatar_full: string | null;
+    admin: 0 | 1;
+    member_count: number;
+    muted: 0 | 1;
   };
 }
 
@@ -244,6 +247,29 @@ export interface PostCreateGroupReturn extends ResponseType {
   can_send_messages: boolean;
 }
 
+export interface PostUpdateGroupSettings {
+  token: string;
+  group_token: string;
+  members_can_edit_settings?: 0 | 1;
+  members_can_add_new_members?: 0 | 1;
+  members_can_send_messages?: 0 | 1;
+  members_can_see_members?: 0 | 1;
+  name?: string;
+  description?: string;
+  avatar?: { uri: string; type: string; name?: string };
+}
+
+export interface PostGetGroupMessageStatusReturn {
+  result: string;
+  status: {
+    uid: number;
+    name: string;
+    avatar: string | null;
+    status: 1 | 2 | 3 | 4;
+    datetime: string | null;
+  }[];
+}
+
 export const chatApi = {
   searchUsers: (token: string, search: string) =>
     request.postForm<PostSearchUsersReturn>(API.SEARCH_USERS, { token, search }),
@@ -386,5 +412,15 @@ export const chatApi = {
   getGroupSettings: (token: string, group_token: string) =>
     request.postForm<PostGetGroupSettingsReturn>(API.GET_GROUP_SETTINGS, { token, group_token }),
   getGroupMembers: (token: string, group_token: string) =>
-    request.postForm<PostGetGroupMembersReturn>(API.GET_GROUP_MEMBERS, { token, group_token })
+    request.postForm<PostGetGroupMembersReturn>(API.GET_GROUP_MEMBERS, { token, group_token }),
+  updateGroupSettings: (data: PostUpdateGroupSettings) =>
+    request.postForm<ResponseType>(API.UPDATE_GROUP_SETTINGS, data),
+  getGroupMessageStatus: (token: string, group_token: string, message_id: number) =>
+    request.postForm<PostGetGroupMessageStatusReturn>(API.GET_GROUP_MESSAGE_STATUS, {
+      token,
+      group_token,
+      message_id
+    }),
+  removeGroupFromList: (token: string, group_token: string) =>
+    request.postForm<ResponseType>(API.REMOVE_GROUP_FROM_LIST, { token, group_token })
 };

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

@@ -45,5 +45,9 @@ export const chatQueryKeys = {
   getGroupSettings: (token: string, group_token: string) =>
     ['getGroupSettings', token, group_token] as const,
   getGroupMembers: (token: string, group_token: string) =>
-    ['getGroupMembers', token, group_token] as const
+    ['getGroupMembers', token, group_token] as const,
+  updateGroupSettings: () => ['updateGroupSettings'] as const,
+  getGroupMessageStatus: (token: string, group_token: string, message_id: number) =>
+    ['getGroupMessageStatus', token, group_token, message_id] as const,
+  removeGroupFromList: () => ['removeGroupFromList'] as const
 };

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

@@ -33,3 +33,6 @@ export * from './use-post-set-group-admin';
 export * from './use-post-remove-from-group';
 export * from './use-post-get-group-settings';
 export * from './use-post-get-group-members';
+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';

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

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

+ 22 - 0
src/modules/api/chat/queries/use-post-remove-group-chat-from-conversation-list.tsx

@@ -0,0 +1,22 @@
+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 usePostRemoveGroupFromListMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; group_token: string },
+    ResponseType
+  >({
+    mutationKey: chatQueryKeys.removeGroupFromList(),
+    mutationFn: async (data) => {
+      const response = await chatApi.removeGroupFromList(data.token, data.group_token);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/chat/queries/use-post-update-group-settings.tsx

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

+ 421 - 0
src/screens/InAppScreens/MessagesScreen/Components/EditGroupModal.tsx

@@ -0,0 +1,421 @@
+import React, { useState } from 'react';
+import {
+  View,
+  StyleSheet,
+  ScrollView,
+  TouchableOpacity,
+  ActivityIndicator,
+  Text,
+  Image
+} from 'react-native';
+import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import * as yup from 'yup';
+import * as ImagePicker from 'expo-image-picker';
+import { chatStyles } from './styles';
+import { Colors } from 'src/theme';
+import { AvatarWithInitials, Input } from 'src/components';
+import Checkbox from 'expo-checkbox';
+import CameraIcon from 'assets/icons/messages/camera.svg';
+import { API_HOST } from 'src/constants';
+import { FlashList } from '@shopify/flash-list';
+import { Formik } from 'formik';
+import { useNavigation } from '@react-navigation/native';
+import { usePostUpdateGroupSettingsMutation, usePostRemoveFromGroupMutation } from '@api/chat';
+import { getFontSize } from 'src/utils';
+import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
+
+const SettingsSchema = yup.object({
+  name: yup
+    .string()
+    .required('name is required')
+    .min(3, 'group name should be at least 3 characters'),
+  description: yup.string().optional().max(8000, 'description should not exceed 8000 characters')
+});
+
+const SearchModal = () => {
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [data, setData] = useState<any>(null);
+  const [image, setImage] = useState<ImagePicker.ImagePickerAsset | null>(null);
+  const { mutateAsync: editGroup } = usePostUpdateGroupSettingsMutation();
+  const { mutateAsync: removeFromGroup } = usePostRemoveFromGroupMutation();
+
+  const [canEdit, setCanEdit] = useState(false);
+  const [canSend, setCanSend] = useState(false);
+  const [canAdd, setCanAdd] = useState(false);
+  const [canSee, setCanSee] = useState(false);
+  const [filteredUsers, setFilteredUsers] = useState<any[]>([]);
+
+  const handleSheetOpen = (payload: any) => {
+    setData(payload);
+    setCanEdit(payload?.settings.members_can_edit_settings === 1);
+    setCanSend(payload?.settings.members_can_send_messages === 1);
+    setCanAdd(payload?.settings.members_can_add_new_members === 1);
+    setCanSee(payload?.settings.members_can_see_members === 1);
+    setFilteredUsers(payload?.members ?? []);
+  };
+
+  const pickImage = async () => {
+    let result = await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.Images,
+      allowsEditing: true,
+      aspect: [4, 3],
+      quality: 1
+    });
+
+    if (!result.canceled) {
+      setImage(result.assets[0]);
+    }
+  };
+
+  const toggleUserSelection = (user: any) => {
+    const isSelected = filteredUsers.some((selected) => selected.uid === user.uid);
+    if (isSelected) {
+      setFilteredUsers((prev) => prev.filter((selected) => selected.uid !== user.uid));
+    } else {
+      setFilteredUsers((prev) => [...prev, user]);
+    }
+  };
+
+  const renderUserItem = ({ item }: { item: any }) => {
+    const isSelected = filteredUsers.some((selected) => selected.uid === item.uid);
+
+    return (
+      <TouchableOpacity style={styles.userItem} onPress={() => toggleUserSelection(item)}>
+        {item.avatar ? (
+          <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
+        ) : (
+          <AvatarWithInitials
+            text={item.name?.split(' ')[0][0] + item.name?.split(' ')[1][0]}
+            flag={API_HOST + item?.flag1}
+            size={36}
+            fontSize={16}
+            borderColor={Colors.LIGHT_GRAY}
+            borderWidth={1}
+          />
+        )}
+        <View style={styles.userDetails}>
+          <Text style={styles.userName}>{item.name}</Text>
+        </View>
+        {item.admin === 1 && (
+          <Text
+            style={{
+              fontSize: getFontSize(10),
+              fontWeight: '600',
+              color: Colors.LIGHT_GRAY
+            }}
+          >
+            Admin
+          </Text>
+        )}
+        <View style={styles.unselectedCircle}>
+          {isSelected && <CheckSvg fill={Colors.DARK_BLUE} height={20} width={20} />}
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  return (
+    <ActionSheet
+      id="edit-group-modal"
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      closeOnTouchBackdrop={false}
+      keyboardHandlerEnabled={false}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+      onClose={() => {
+        setImage(null);
+        data && data.refetch();
+      }}
+    >
+      <Formik
+        validationSchema={SettingsSchema}
+        initialValues={{
+          name: data?.settings?.name ?? '',
+          description: data?.settings?.description ?? ''
+        }}
+        onSubmit={async (values) => {
+          if (!data) return;
+
+          setIsSubmitting(true);
+
+          const removedUsers = data?.members
+            ?.filter((member: any) => !filteredUsers.some((user) => user.uid === member.uid))
+            ?.map((member: any) => member.uid);
+
+          const groupData: any = {
+            token: data.token,
+            group_token: data.groupToken,
+            name: values.name,
+            description: values.description,
+            members_can_edit_settings: canEdit ? 1 : 0,
+            members_can_send_messages: canSend ? 1 : 0,
+            members_can_add_new_members: canAdd ? 1 : 0,
+            members_can_see_members: canSee ? 1 : 0
+          };
+
+          if (image && image.uri) {
+            groupData.group_avatar = {
+              type: image.type || 'image',
+              uri: image.uri,
+              name: image.uri.split('/').pop()!
+            };
+          }
+
+          if (removedUsers.length > 0) {
+            removedUsers.forEach(async (userId: number) => {
+              await removeFromGroup(
+                {
+                  token: data.token,
+                  group_token: data.groupToken,
+                  uid: userId
+                },
+                {
+                  onSuccess: (res) => {
+                    console.log('res', res);
+                    data && data.refetchMembers();
+                  }
+                }
+              );
+            });
+          }
+
+          await editGroup(groupData, {
+            onSuccess: () => {
+              setIsSubmitting(false);
+              SheetManager.hide('edit-group-modal');
+            },
+            onError: () => {
+              setIsSubmitting(false);
+            }
+          });
+        }}
+      >
+        {(props) => {
+          return (
+            <View style={chatStyles.container}>
+              <View style={chatStyles.header}>
+                <TouchableOpacity
+                  onPress={() => SheetManager.hide('edit-group-modal')}
+                  style={styles.backButton}
+                >
+                  <Text style={chatStyles.headerText}>Back</Text>
+                </TouchableOpacity>
+
+                {isSubmitting ? (
+                  <ActivityIndicator size="small" color={Colors.DARK_BLUE} style={styles.loader} />
+                ) : (
+                  <TouchableOpacity onPress={() => props.handleSubmit()} style={styles.saveButton}>
+                    <Text style={chatStyles.headerText}>Save</Text>
+                  </TouchableOpacity>
+                )}
+              </View>
+
+              <ScrollView
+                showsVerticalScrollIndicator={false}
+                style={{ flex: 1 }}
+                contentContainerStyle={{ gap: 16 }}
+              >
+                <View style={styles.photoContainer}>
+                  <TouchableOpacity onPress={pickImage} style={chatStyles.photoContainer}>
+                    {image || data?.settings?.avatar ? (
+                      <>
+                        <Image
+                          source={{ uri: image ? image.uri : API_HOST + data?.settings?.avatar }}
+                          style={styles.groupPhotoImage}
+                        />
+                        <Text style={chatStyles.photoText}>Change photo</Text>
+                      </>
+                    ) : (
+                      <>
+                        <View
+                          style={[chatStyles.groupPhoto, { backgroundColor: Colors.FILL_LIGHT }]}
+                        >
+                          <CameraIcon width={36} height={36} fill={Colors.LIGHT_GRAY} />
+                        </View>
+                        <Text style={chatStyles.photoText}>Add photo</Text>
+                      </>
+                    )}
+                  </TouchableOpacity>
+                </View>
+
+                <Input
+                  placeholder="Add group name"
+                  value={props.values.name}
+                  inputMode="text"
+                  onChange={props.handleChange('name')}
+                  onBlur={props.handleBlur('name')}
+                  header="Group name"
+                  formikError={props.touched.name && (props.errors.name as string)}
+                />
+
+                <Input
+                  placeholder="Add group description"
+                  value={props.values.description}
+                  onChange={props.handleChange('description')}
+                  onBlur={props.handleBlur('description')}
+                  header="Description"
+                  multiline
+                  height={58}
+                  formikError={props.touched.description && (props.errors.description as string)}
+                />
+
+                <View>
+                  <Text style={chatStyles.title}>Members can</Text>
+
+                  <View style={chatStyles.optionsContainer}>
+                    <TouchableOpacity
+                      style={chatStyles.option}
+                      onPress={() => setCanEdit(!canEdit)}
+                    >
+                      <Text style={chatStyles.optionText}>Edit group settings</Text>
+                      <Checkbox
+                        value={canEdit}
+                        color={Colors.DARK_BLUE}
+                        style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
+                        onValueChange={() => setCanEdit(!canEdit)}
+                      />
+                    </TouchableOpacity>
+
+                    <TouchableOpacity
+                      style={chatStyles.option}
+                      onPress={() => setCanSend(!canSend)}
+                    >
+                      <Text style={chatStyles.optionText}>Send messages</Text>
+                      <Checkbox
+                        value={canSend}
+                        color={Colors.DARK_BLUE}
+                        style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
+                        onValueChange={() => setCanSend(!canSend)}
+                      />
+                    </TouchableOpacity>
+
+                    <TouchableOpacity style={chatStyles.option} onPress={() => setCanAdd(!canAdd)}>
+                      <Text style={chatStyles.optionText}>Add new members</Text>
+                      <Checkbox
+                        value={canAdd}
+                        color={Colors.DARK_BLUE}
+                        style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
+                        onValueChange={() => setCanAdd(!canAdd)}
+                      />
+                    </TouchableOpacity>
+
+                    <TouchableOpacity style={chatStyles.option} onPress={() => setCanSee(!canSee)}>
+                      <Text style={chatStyles.optionText}>See members</Text>
+                      <Checkbox
+                        value={canSee}
+                        color={Colors.DARK_BLUE}
+                        style={{ backgroundColor: Colors.WHITE, borderRadius: 4 }}
+                        onValueChange={() => setCanSee(!canSee)}
+                      />
+                    </TouchableOpacity>
+                  </View>
+                </View>
+
+                {data?.settings?.admin === 1 ? (
+                  <View>
+                    <Text style={chatStyles.title}>Members: {data?.members?.length}</Text>
+                    {data?.members?.length > 0 && (
+                      <FlashList
+                        viewabilityConfig={{
+                          waitForInteraction: true,
+                          itemVisiblePercentThreshold: 50,
+                          minimumViewTime: 1000
+                        }}
+                        data={data?.members || []}
+                        renderItem={renderUserItem}
+                        keyExtractor={(item) => item.uid.toString()}
+                        estimatedItemSize={100}
+                        extraData={filteredUsers}
+                        showsVerticalScrollIndicator={false}
+                        contentContainerStyle={{ paddingBottom: 16 }}
+                      />
+                    )}
+                  </View>
+                ) : null}
+              </ScrollView>
+            </View>
+          );
+        }}
+      </Formik>
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  sheetContainer: {
+    height: '95%',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 16
+  },
+  backButton: {
+    paddingVertical: 16,
+    paddingHorizontal: 6
+  },
+  saveButton: {
+    paddingVertical: 16,
+    paddingHorizontal: 6
+  },
+  loader: {
+    padding: 10
+  },
+  groupPhotoImage: {
+    width: 80,
+    height: 80,
+    borderRadius: 40,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  errorBorder: {
+    borderColor: Colors.RED,
+    borderWidth: 1
+  },
+  photoContainer: {
+    alignItems: 'center',
+    gap: 8
+  },
+  userDetails: {
+    flex: 1
+  },
+  userName: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'montserrat-700'
+  },
+  userSubtitle: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontFamily: 'montserrat-500'
+  },
+  userItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    backgroundColor: Colors.FILL_LIGHT,
+    gap: 8,
+    borderRadius: 8,
+    marginBottom: 6
+  },
+  avatar: {
+    width: 36,
+    height: 36,
+    borderRadius: 18,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY
+  },
+  unselectedCircle: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    justifyContent: 'center',
+    alignItems: 'center'
+  }
+});
+
+export default SearchModal;

+ 51 - 16
src/screens/InAppScreens/MessagesScreen/Components/MoreModal.tsx

@@ -14,7 +14,8 @@ import {
   usePostSetBlockMutation,
   usePostSetMuteMutation,
   usePostSetMuteForGroupMutation,
-  usePostLeaveGroupMutation
+  usePostLeaveGroupMutation,
+  usePostRemoveGroupFromListMutation
 } from '@api/chat';
 import { useChatStore } from 'src/stores/chatStore';
 import TrashIcon from 'assets/icons/travels-screens/trash-solid.svg';
@@ -45,6 +46,7 @@ const MoreModal = () => {
   const { mutateAsync: reportUser } = usePostReportConversationMutation();
   const { mutateAsync: muteGroup } = usePostSetMuteForGroupMutation();
   const { mutateAsync: leaveGroup } = usePostLeaveGroupMutation();
+  const { mutateAsync: removeGroupFromList } = usePostRemoveGroupFromListMutation();
 
   const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
 
@@ -201,6 +203,29 @@ const MoreModal = () => {
     }, 300);
   };
 
+  const handleDeleteGroup = async () => {
+    if (!chatData) return;
+
+    setShouldOpenWarningModal({
+      title: `Delete ${name}`,
+      message: `Are you sure you want to delete this group chat?\nThis action will remove the chat from your history, but it won't affect other participants.`,
+      action: async () => {
+        chatData.groupToken &&
+          (await removeGroupFromList({
+            token: chatData.token,
+            group_token: chatData.groupToken
+          }));
+
+        chatData.refetch();
+      }
+    });
+
+    setTimeout(() => {
+      SheetManager.hide('more-modal');
+      setShouldOpenWarningModal(null);
+    }, 300);
+  };
+
   return (
     <ActionSheet
       id="more-modal"
@@ -268,18 +293,18 @@ const MoreModal = () => {
           )}
 
           <View style={[styles.optionsContainer, { paddingVertical: 0, gap: 0 }]}>
-            {chatData?.userType === 'normal' && (
-              <>
-                {chatData?.groupToken && (
-                  <TouchableOpacity
-                    style={[styles.option, styles.dangerOption]}
-                    onPress={handleLeaveGroup}
-                  >
-                    <Text style={[styles.optionText, styles.dangerText]}>Leave group</Text>
-                    <ExitIcon fill={Colors.RED} width={16} />
-                  </TouchableOpacity>
-                )}
+            {chatData?.groupToken && (
+              <TouchableOpacity
+                style={[styles.option, styles.dangerOption]}
+                onPress={handleLeaveGroup}
+              >
+                <Text style={[styles.optionText, styles.dangerText]}>Leave group chat</Text>
+                <ExitIcon fill={Colors.RED} width={16} />
+              </TouchableOpacity>
+            )}
 
+            {chatData?.userType === 'normal' && chatData?.uid && (
+              <>
                 <TouchableOpacity
                   style={[styles.option, styles.dangerOption]}
                   onPress={handleReport}
@@ -298,10 +323,20 @@ const MoreModal = () => {
               </>
             )}
 
-            <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleDelete}>
-              <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
-              <TrashIcon fill={Colors.RED} width={18} height={18} />
-            </TouchableOpacity>
+            {chatData?.uid ? (
+              <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleDelete}>
+                <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
+                <TrashIcon fill={Colors.RED} width={18} height={18} />
+              </TouchableOpacity>
+            ) : (
+              <TouchableOpacity
+                style={[styles.option, styles.dangerOption]}
+                onPress={handleDeleteGroup}
+              >
+                <Text style={[styles.optionText, styles.dangerText]}>Delete group chat</Text>
+                <TrashIcon fill={Colors.RED} width={18} height={18} />
+              </TouchableOpacity>
+            )}
           </View>
         </View>
       )}

+ 2 - 157
src/screens/InAppScreens/MessagesScreen/Components/RouteAddGroup.tsx

@@ -1,13 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import {
-  View,
-  Text,
-  TouchableOpacity,
-  StyleSheet,
-  Image,
-  ActivityIndicator,
-  ScrollView
-} from 'react-native';
+import { View, Text, TouchableOpacity, Image, ActivityIndicator, ScrollView } from 'react-native';
 import { useNavigation } from '@react-navigation/native';
 import { Colors } from 'src/theme';
 import { Input } from 'src/components';
@@ -21,13 +13,13 @@ import Checkbox from 'expo-checkbox';
 
 import { FlashList } from '@shopify/flash-list';
 import { useSheetRouter } from 'react-native-actions-sheet';
-import { getFontSize } from 'src/utils';
 
 import CloseIcon from 'assets/icons/close.svg';
 import CameraIcon from 'assets/icons/messages/camera.svg';
 import { Formik } from 'formik';
 import { useGroupChatStore } from 'src/stores/groupChatStore';
 import { NAVIGATION_PAGES } from 'src/types';
+import { chatStyles as styles } from './styles';
 
 const ProfileSchema = yup.object({
   name: yup
@@ -146,7 +138,6 @@ const RouteAddGroup = () => {
             router?.close();
           },
           onError: (err) => {
-            console.log('err', err);
             setIsSubmitting(false);
           }
         });
@@ -333,150 +324,4 @@ const RouteAddGroup = () => {
   );
 };
 
-const styles = StyleSheet.create({
-  container: {
-    gap: 16,
-    height: '100%',
-    backgroundColor: Colors.WHITE
-  },
-  header: {
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center'
-  },
-  headerText: {
-    color: Colors.DARK_BLUE,
-    fontSize: getFontSize(14),
-    fontWeight: '700'
-  },
-  photoContainer: {
-    alignItems: 'center',
-    gap: 8
-  },
-  groupPhoto: {
-    width: 80,
-    height: 80,
-    borderRadius: 40,
-    alignItems: 'center',
-    justifyContent: 'center'
-  },
-  photoText: {
-    color: Colors.DARK_BLUE,
-    fontSize: 12,
-    fontWeight: '700'
-  },
-  input: {
-    marginBottom: 12
-  },
-  userItem: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    paddingVertical: 8,
-    paddingHorizontal: 12,
-    backgroundColor: Colors.FILL_LIGHT,
-    gap: 8,
-    borderRadius: 8,
-    marginBottom: 6
-  },
-  avatar: {
-    width: 36,
-    height: 36,
-    borderRadius: 18,
-    borderWidth: 1,
-    borderColor: Colors.LIGHT_GRAY
-  },
-  userName: {
-    color: Colors.DARK_BLUE,
-    fontSize: getFontSize(14),
-    fontFamily: 'montserrat-700'
-  },
-  userSubtitle: {
-    color: Colors.DARK_BLUE,
-    fontSize: 14,
-    fontFamily: 'montserrat-500'
-  },
-  userNM: {
-    color: Colors.DARK_BLUE,
-    fontSize: 14,
-    fontFamily: 'montserrat-700',
-    marginRight: 12
-  },
-  unselectedCircle: {
-    width: 20,
-    height: 20,
-    borderRadius: 10,
-    borderWidth: 1,
-    borderColor: Colors.LIGHT_GRAY,
-    justifyContent: 'center',
-    alignItems: 'center'
-  },
-  selectedUsersList: {
-    paddingTop: 12
-  },
-  selectedUserContainer: {
-    position: 'relative',
-    width: '100%',
-    alignItems: 'center',
-    paddingBottom: 12
-  },
-  userContainer: {},
-  selectedAvatar: {
-    width: 60,
-    height: 60,
-    borderRadius: 30,
-    borderWidth: 1,
-    borderColor: Colors.LIGHT_GRAY
-  },
-  removeIcon: {
-    position: 'absolute',
-    top: -4,
-    right: -4,
-    width: 22,
-    height: 22,
-    borderRadius: 11,
-    borderWidth: 1,
-    borderColor: Colors.WHITE,
-    backgroundColor: Colors.RED,
-    justifyContent: 'center',
-    alignItems: 'center',
-    zIndex: 1
-  },
-  usersRow: {
-    flex: 1,
-    backgroundColor: Colors.FILL_LIGHT,
-    borderRadius: 8,
-    marginBottom: 24
-  },
-  textError: {
-    color: Colors.RED,
-    fontSize: getFontSize(12),
-    fontFamily: 'redhat-600',
-    marginTop: 5
-  },
-  optionsContainer: {
-    paddingHorizontal: 8,
-    borderRadius: 8,
-    backgroundColor: Colors.FILL_LIGHT
-  },
-  option: {
-    flexDirection: 'row',
-    alignItems: 'center',
-    justifyContent: 'space-between',
-    paddingVertical: 10,
-    borderBottomWidth: 1,
-    borderBlockColor: Colors.WHITE
-  },
-  optionText: {
-    fontSize: getFontSize(12),
-    fontWeight: '600',
-    color: Colors.DARK_BLUE
-  },
-  title: {
-    color: Colors.DARK_BLUE,
-    fontSize: getFontSize(14),
-    fontFamily: 'redhat-700',
-    marginBottom: 5
-  }
-});
-
 export default RouteAddGroup;

+ 200 - 0
src/screens/InAppScreens/MessagesScreen/Components/TypingIndicator.tsx

@@ -0,0 +1,200 @@
+import React, { useCallback, useEffect, useState, useMemo } from 'react';
+import { View, StyleSheet, Text } from 'react-native';
+import Animated, {
+  runOnJS,
+  useAnimatedStyle,
+  useSharedValue,
+  withDelay,
+  withRepeat,
+  withSequence,
+  withTiming
+} from 'react-native-reanimated';
+
+import { Colors } from 'src/theme';
+
+interface TypingIndicatorProps {
+  isTyping?: string | null;
+}
+
+const DotsAnimation = ({ name }: { name: string }) => {
+  const dot1 = useSharedValue(0);
+  const dot2 = useSharedValue(0);
+  const dot3 = useSharedValue(0);
+
+  const firstName = name?.split(' ')[0];
+
+  const topY = useMemo(() => -5, []);
+  const bottomY = useMemo(() => 5, []);
+  const duration = useMemo(() => 500, []);
+
+  const dot1Style = useAnimatedStyle(
+    () => ({
+      transform: [
+        {
+          translateY: dot1.value
+        }
+      ]
+    }),
+    [dot1]
+  );
+
+  const dot2Style = useAnimatedStyle(
+    () => ({
+      transform: [
+        {
+          translateY: dot2.value
+        }
+      ]
+    }),
+    [dot2]
+  );
+
+  const dot3Style = useAnimatedStyle(
+    () => ({
+      transform: [
+        {
+          translateY: dot3.value
+        }
+      ]
+    }),
+    [dot3]
+  );
+
+  useEffect(() => {
+    dot1.value = withRepeat(
+      withSequence(withTiming(topY, { duration }), withTiming(bottomY, { duration })),
+      0,
+      true
+    );
+  }, [dot1, topY, bottomY, duration]);
+
+  useEffect(() => {
+    dot2.value = withDelay(
+      100,
+      withRepeat(
+        withSequence(withTiming(topY, { duration }), withTiming(bottomY, { duration })),
+        0,
+        true
+      )
+    );
+  }, [dot2, topY, bottomY, duration]);
+
+  useEffect(() => {
+    dot3.value = withDelay(
+      200,
+      withRepeat(
+        withSequence(withTiming(topY, { duration }), withTiming(bottomY, { duration })),
+        0,
+        true
+      )
+    );
+  }, [dot3, topY, bottomY, duration]);
+
+  return (
+    <View style={[styles.fill, styles.centerItems, styles.dots]}>
+      <Text
+        style={{
+          color: 'rgba(0, 0, 0, 0.38)',
+          marginRight: 4,
+          fontStyle: 'italic',
+          fontSize: 12,
+          textAlignVertical: 'center'
+        }}
+      >
+        {firstName} is typing
+      </Text>
+      <Animated.View style={[styles.dot, dot1Style]} />
+      <Animated.View style={[styles.dot, dot2Style]} />
+      <Animated.View style={[styles.dot, dot3Style]} />
+    </View>
+  );
+};
+
+const TypingIndicator = ({ isTyping }: TypingIndicatorProps) => {
+  const yCoords = useSharedValue(200);
+  const heightScale = useSharedValue(0);
+  const marginScale = useSharedValue(0);
+
+  const [isVisible, setIsVisible] = useState((isTyping ? true : false) as boolean);
+
+  const containerStyle = useAnimatedStyle(
+    () => ({
+      transform: [
+        {
+          translateY: yCoords.value
+        }
+      ],
+      height: heightScale.value,
+      marginBottom: marginScale.value
+    }),
+    [yCoords, heightScale, marginScale]
+  );
+
+  const slideIn = useCallback(() => {
+    const duration = 250;
+
+    yCoords.value = withTiming(0, { duration });
+    heightScale.value = withTiming(30, { duration });
+    marginScale.value = withTiming(8, { duration });
+  }, [yCoords, heightScale, marginScale]);
+
+  const slideOut = useCallback(() => {
+    const duration = 250;
+
+    yCoords.value = withTiming(200, { duration }, (isFinished) => {
+      if (isFinished) runOnJS(setIsVisible)(false);
+    });
+    heightScale.value = withTiming(0, { duration });
+    marginScale.value = withTiming(0, { duration });
+  }, [yCoords, heightScale, marginScale]);
+
+  useEffect(() => {
+    if (isVisible)
+      if (isTyping) slideIn();
+      else slideOut();
+  }, [isVisible, isTyping, slideIn, slideOut]);
+
+  useEffect(() => {
+    if (isTyping) setIsVisible(true);
+  }, [isTyping]);
+
+  if (!isVisible) return null;
+
+  return (
+    <Animated.View style={[styles.container, containerStyle]}>
+      <DotsAnimation name={isTyping as string} />
+    </Animated.View>
+  );
+};
+
+const styles = StyleSheet.create({
+  fill: {
+    flex: 1
+  },
+  centerItems: {
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  container: {
+    marginLeft: 8,
+    borderRadius: 15,
+    backgroundColor: Colors.FILL_LIGHT,
+    alignSelf: 'flex-start',
+    paddingHorizontal: 10,
+    paddingVertical: 4
+  },
+  dots: {
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  dot: {
+    marginLeft: 2,
+    marginRight: 2,
+    borderRadius: 3,
+    width: 6,
+    height: 6,
+    backgroundColor: 'rgba(0, 0, 0, 0.38)'
+  }
+});
+
+export default TypingIndicator;

+ 149 - 0
src/screens/InAppScreens/MessagesScreen/Components/styles.tsx

@@ -0,0 +1,149 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const chatStyles = StyleSheet.create({
+  container: {
+    gap: 16,
+    height: '100%',
+    backgroundColor: Colors.WHITE
+  },
+  header: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  headerText: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontWeight: '700'
+  },
+  photoContainer: {
+    alignItems: 'center',
+    gap: 8
+  },
+  groupPhoto: {
+    width: 80,
+    height: 80,
+    borderRadius: 40,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  photoText: {
+    color: Colors.DARK_BLUE,
+    fontSize: 12,
+    fontWeight: '700'
+  },
+  input: {
+    marginBottom: 12
+  },
+  userItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    backgroundColor: Colors.FILL_LIGHT,
+    gap: 8,
+    borderRadius: 8,
+    marginBottom: 6
+  },
+  avatar: {
+    width: 36,
+    height: 36,
+    borderRadius: 18,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY
+  },
+  userName: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'montserrat-700'
+  },
+  userSubtitle: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontFamily: 'montserrat-500'
+  },
+  userNM: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontFamily: 'montserrat-700',
+    marginRight: 12
+  },
+  unselectedCircle: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  selectedUsersList: {
+    paddingTop: 12
+  },
+  selectedUserContainer: {
+    position: 'relative',
+    width: '100%',
+    alignItems: 'center',
+    paddingBottom: 12
+  },
+  userContainer: {},
+  selectedAvatar: {
+    width: 60,
+    height: 60,
+    borderRadius: 30,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY
+  },
+  removeIcon: {
+    position: 'absolute',
+    top: -4,
+    right: -4,
+    width: 22,
+    height: 22,
+    borderRadius: 11,
+    borderWidth: 1,
+    borderColor: Colors.WHITE,
+    backgroundColor: Colors.RED,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 1
+  },
+  usersRow: {
+    flex: 1,
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 8,
+    marginBottom: 24
+  },
+  textError: {
+    color: Colors.RED,
+    fontSize: getFontSize(12),
+    fontFamily: 'redhat-600',
+    marginTop: 5
+  },
+  optionsContainer: {
+    paddingHorizontal: 8,
+    borderRadius: 8,
+    backgroundColor: Colors.FILL_LIGHT
+  },
+  option: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingVertical: 10,
+    borderBottomWidth: 1,
+    borderBlockColor: Colors.WHITE
+  },
+  optionText: {
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    color: Colors.DARK_BLUE
+  },
+  title: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700',
+    marginBottom: 5
+  }
+});

+ 30 - 15
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -31,7 +31,7 @@ import {
 } from 'react-native-gifted-chat';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
-import { AvatarWithInitials, Header, WarningModal } from 'src/components';
+import { Header, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
 import { Audio } from 'expo-av';
@@ -56,6 +56,7 @@ import { API_HOST, 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';
@@ -148,7 +149,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
   const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
   const [isRerendering, setIsRerendering] = useState<boolean>(false);
-  const [isTyping, setIsTyping] = useState<boolean>(false);
+  const [isTyping, setIsTyping] = useState<string | null>(null);
 
   const messageRefs = useRef<{ [key: string]: any }>({});
   const flatList = useRef<FlatList | null>(null);
@@ -648,13 +649,22 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   const handleWebSocketMessage = (data: any) => {
     switch (data.action) {
       case 'new_message':
-        if (data.conversation_with === group_token && data.message) {
+        if (data.group_token === group_token && data.message) {
           const newMessage = mapApiMessageToGiftedMessage(data.message);
           setMessages((previousMessages) => {
             const messageExists =
               previousMessages && previousMessages.some((msg) => msg._id === newMessage._id);
             if (!messageExists) {
-              return GiftedChat.append(previousMessages ?? [], [newMessage]);
+              return GiftedChat.append(previousMessages ?? [], [
+                {
+                  ...newMessage,
+                  user: {
+                    _id: data.uid,
+                    name: data.name,
+                    avatar: API_HOST + data.avatar
+                  }
+                }
+              ]);
             }
             return previousMessages;
           });
@@ -662,37 +672,39 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         break;
 
       case 'new_reaction':
-        if (data.conversation_with === group_token && data.reaction) {
+        if (data.group_token === group_token && data.reaction) {
+          // todo: name
           updateMessageWithReaction(data.reaction);
         }
         break;
 
       case 'unreact':
-        if (data.conversation_with === group_token && data.unreacted_message_id) {
+        if (data.group_token === group_token && data.unreacted_message_id) {
+          // todo: name
           removeReactionFromMessage(data.unreacted_message_id);
         }
         break;
 
       case 'delete_message':
-        if (data.conversation_with === group_token && data.deleted_message_id) {
+        if (data.group_token === group_token && data.deleted_message_id) {
           removeDeletedMessage(data.deleted_message_id);
         }
         break;
 
       case 'is_typing':
-        if (data.conversation_with === group_token) {
-          setIsTyping(true);
+        if (data.group_token === group_token && data.uid !== +currentUserId) {
+          setIsTyping(data.name);
         }
         break;
 
       case 'stopped_typing':
-        if (data.conversation_with === group_token) {
-          setIsTyping(false);
+        if (data.group_token === group_token) {
+          setIsTyping(null);
         }
         break;
 
       case 'messages_read':
-        if (data.conversation_with === group_token && data.read_messages_ids) {
+        if (data.group_token === group_token && data.read_messages_ids) {
           setMessages(
             (prevMessages) =>
               prevMessages?.map((msg) => {
@@ -765,7 +777,9 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   useEffect(() => {
     const pingInterval = setInterval(() => {
       if (socket.current && socket.current.readyState === WebSocket.OPEN) {
-        socket.current.send(JSON.stringify({ action: 'ping', conversation_with: group_token }));
+        socket.current.send(
+          JSON.stringify({ action: 'ping', conversation_with_group: group_token })
+        );
       } else {
         socket.current = new WebSocket(WEBSOCKET_URL);
         socket.current.onopen = () => {
@@ -797,7 +811,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     if (socket.current && socket.current.readyState === WebSocket.OPEN) {
       const data: any = {
         action,
-        conversation_with: group_token
+        conversation_with_group: group_token
       };
 
       if (action === 'new_message' && message) {
@@ -1826,7 +1840,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             minComposerHeight={34}
             onInputTextChanged={(text) => handleTyping(text.length > 0)}
             textInputRef={textInputRef}
-            isTyping={isTyping}
+            isTyping={isTyping ? true : false}
+            renderTypingIndicator={() => <TypingIndicator isTyping={isTyping} />}
             renderSend={(props) => (
               <View style={styles.sendBtn}>
                 {props.text?.trim() && (

+ 160 - 51
src/screens/InAppScreens/MessagesScreen/GroupSettingsScreen/index.tsx

@@ -1,20 +1,36 @@
-import React, { FC, useCallback, useEffect, useState } from 'react';
+import React, { FC, useEffect, useState } from 'react';
 import { ScrollView, Text, TouchableOpacity, View, Image, StyleSheet } from 'react-native';
-import { NavigationProp, useFocusEffect } from '@react-navigation/native';
+import { NavigationProp } from '@react-navigation/native';
 import ImageView from 'react-native-image-viewing';
 import { storage, StoreType } from 'src/storage';
-import { PageWrapper, Header, Loading, Input, AvatarWithInitials } from 'src/components';
-import { usePostGetGroupMembersQuery, usePostGetGroupSettingsQuery } from '@api/chat';
+import {
+  PageWrapper,
+  Header,
+  Loading,
+  Input,
+  AvatarWithInitials,
+  WarningModal
+} from 'src/components';
+import {
+  usePostGetGroupMembersQuery,
+  usePostGetGroupSettingsQuery,
+  usePostLeaveGroupMutation,
+  usePostRemoveGroupFromListMutation,
+  usePostSetMuteForGroupMutation
+} from '@api/chat';
 import { Colors } from 'src/theme';
 import { API_HOST } from 'src/constants';
 import GroupIcon from 'assets/icons/messages/group-chat.svg';
 import { getFontSize } from 'src/utils';
-import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
 import ExitIcon from 'assets/icons/messages/exit.svg';
 import TrashIcon from 'assets/icons/travels-screens/trash-solid.svg';
-import BanIcon from 'assets/icons/messages/ban.svg';
 import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import EditIcon from 'assets/icons/travels-screens/pen-to-square.svg';
+import UserPlusIcon from 'assets/icons/user-plus.svg';
+import { NAVIGATION_PAGES } from 'src/types';
+import { SheetManager } from 'react-native-actions-sheet';
+import EditGroupModal from '../Components/EditGroupModal';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -26,17 +42,111 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
   const groupToken = route.params.groupToken;
   const insets = useSafeAreaInsets();
   const [canSeeMembers, setCanSeeMembers] = useState(false);
-  const { data } = usePostGetGroupSettingsQuery(token, groupToken, true);
-  const { data: members } = usePostGetGroupMembersQuery(token, groupToken, canSeeMembers);
+  const { data, refetch } = usePostGetGroupSettingsQuery(token, groupToken, true);
+  const { data: members, refetch: refetchMembers } = usePostGetGroupMembersQuery(
+    token,
+    groupToken,
+    canSeeMembers
+  );
 
   const [fullSizeImageVisible, setFullSizeImageVisible] = useState(false);
+  const [muted, setMuted] = useState(false);
+  const [modalState, setModalState] = useState({
+    isWarningVisible: false,
+    title: '',
+    buttonTitle: '',
+    message: '',
+    action: () => {}
+  });
+  const { mutateAsync: muteGroup } = usePostSetMuteForGroupMutation();
+  const { mutateAsync: leaveGroup } = usePostLeaveGroupMutation();
+  const { mutateAsync: removeGroupFromList } = usePostRemoveGroupFromListMutation();
 
   useEffect(() => {
     if (data && data.settings) {
-      setCanSeeMembers(data.settings.members_can_see_members === 1);
+      setCanSeeMembers(data.settings.members_can_see_members === 1 || data.settings.admin === 1);
+      setMuted(data.settings.muted === 1);
     }
   }, [data]);
 
+  const handleMute = async () => {
+    await muteGroup(
+      {
+        token,
+        value: muted ? 0 : 1,
+        group_token: groupToken
+      },
+      {
+        onSuccess: () => {
+          setMuted(!muted);
+        }
+      }
+    );
+  };
+
+  const handleLeaveGroup = async () => {
+    if (!data) return;
+
+    setModalState({
+      isWarningVisible: true,
+      title: `Leave group ${data.settings.name}`,
+      buttonTitle: 'Leave',
+      message: `Are you sure you want to leave ${data.settings.name}?`,
+      action: async () => {
+        await leaveGroup(
+          {
+            token,
+            group_token: groupToken
+          },
+          {
+            onSuccess: () => {
+              navigation.navigate(NAVIGATION_PAGES.CHATS_LIST);
+            }
+          }
+        );
+      }
+    });
+  };
+
+  const handleDeleteGroup = async () => {
+    if (!data) return;
+
+    setModalState({
+      isWarningVisible: true,
+      title: `Delete ${data.settings.name}`,
+      buttonTitle: 'Delete',
+      message: `Are you sure you want to delete this group chat?\nThis action will remove the chat from your history, but it won't affect other participants.`,
+      action: async () => {
+        await removeGroupFromList(
+          {
+            token,
+            group_token: groupToken
+          },
+          {
+            onSuccess: () => {
+              navigation.navigate(NAVIGATION_PAGES.CHATS_LIST);
+            }
+          }
+        );
+      }
+    });
+  };
+
+  const openEditModal = () => {
+    if (!data) return;
+
+    SheetManager.show('edit-group-modal', {
+      payload: {
+        settings: data.settings,
+        members: data.settings.admin === 1 ? members?.settings : [],
+        token,
+        groupToken,
+        refetch,
+        refetchMembers
+      }
+    });
+  };
+
   if (!data) return <Loading />;
 
   return (
@@ -44,10 +154,10 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
       <Header
         label={data.settings.name}
         rightElement={
-          data.settings.members_can_edit_settings === 1 ? (
-            <TouchableOpacity
-              style={{ width: 30, height: 30, backgroundColor: 'green' }}
-            ></TouchableOpacity>
+          data.settings.members_can_edit_settings === 1 || data.settings.admin === 1 ? (
+            <TouchableOpacity style={{ padding: 6 }} onPress={openEditModal}>
+              <EditIcon fill={Colors.DARK_BLUE} width={18} height={18} />
+            </TouchableOpacity>
           ) : null
         }
       />
@@ -83,6 +193,9 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
               )}
             </TouchableOpacity>
             <Text style={styles.bigText}>{data.settings.name}</Text>
+            <Text style={{ fontSize: getFontSize(12), fontWeight: '600', color: Colors.DARK_BLUE }}>
+              {data.settings.member_count} nomads
+            </Text>
           </View>
 
           {data.settings.description && (
@@ -105,15 +218,15 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
                 }}
               >
                 <Text style={styles.title}>{members.settings.length} nomads</Text>
-                {data.settings.members_can_add_new_members === 1 ? (
-                  <TouchableOpacity
-                    style={{ width: 30, height: 30, backgroundColor: 'green' }}
-                  ></TouchableOpacity>
+                {data.settings.members_can_add_new_members === 1 || data.settings.admin === 1 ? (
+                  <TouchableOpacity style={{ padding: 6, paddingRight: 0 }}>
+                    <UserPlusIcon fill={Colors.ORANGE} height={18} width={23} />
+                  </TouchableOpacity>
                 ) : null}
               </View>
 
               <View style={{ gap: 6 }}>
-                {(members.settings.length > 4
+                {(data.settings.member_count > 4
                   ? members.settings.slice(0, 4)
                   : members.settings
                 ).map((member, index) => (
@@ -123,7 +236,7 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
                     ) : (
                       <AvatarWithInitials
                         text={`${member.name?.split(' ')[0][0]}${member.name?.split(' ')[1][0]}`}
-                        flag={''}
+                        flag={API_HOST + member?.flag}
                         size={36}
                         fontSize={16}
                         borderColor={Colors.LIGHT_GRAY}
@@ -148,6 +261,19 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
                     </View>
                   </TouchableOpacity>
                 ))}
+                {data.settings.member_count > 4 ? (
+                  <TouchableOpacity style={{ padding: 8, alignItems: 'center' }}>
+                    <Text
+                      style={{
+                        color: Colors.DARK_BLUE,
+                        fontSize: getFontSize(12),
+                        fontWeight: '700'
+                      }}
+                    >
+                      All nomads...
+                    </Text>
+                  </TouchableOpacity>
+                ) : null}
               </View>
             </View>
           ) : null}
@@ -155,11 +281,8 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
 
         <View style={{ gap: 16 }}>
           <View style={styles.optionsContainer}>
-            <TouchableOpacity
-              style={styles.option}
-              //  onPress={handleMute}
-            >
-              <Text style={styles.optionText}>{false ? 'Unmute' : 'Mute'}</Text>
+            <TouchableOpacity style={styles.option} onPress={handleMute}>
+              <Text style={styles.optionText}>{muted ? 'Unmute' : 'Mute'}</Text>
               <BellSlashIcon fill={Colors.DARK_BLUE} />
             </TouchableOpacity>
           </View>
@@ -167,47 +290,32 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
           <View style={[styles.optionsContainer, { paddingVertical: 0, gap: 0 }]}>
             <TouchableOpacity
               style={[styles.option, styles.dangerOption]}
-              // onPress={handleLeaveGroup}
+              onPress={handleLeaveGroup}
             >
-              <Text style={[styles.optionText, styles.dangerText]}>Leave group</Text>
+              <Text style={[styles.optionText, styles.dangerText]}>Leave group chat</Text>
               <ExitIcon fill={Colors.RED} width={16} />
             </TouchableOpacity>
 
             <TouchableOpacity
               style={[styles.option, styles.dangerOption]}
-              // onPress={handleReport}
-            >
-              <Text style={[styles.optionText, styles.dangerText]}>Report</Text>
-              <MegaphoneIcon fill={Colors.RED} />
-            </TouchableOpacity>
-
-            <TouchableOpacity
-              style={[styles.option, styles.dangerOption]}
-              // onPress={handleBlock}
+              onPress={handleDeleteGroup}
             >
-              <Text style={[styles.optionText, styles.dangerText]}>Block</Text>
-              <BanIcon fill={Colors.RED} />
-            </TouchableOpacity>
-
-            <TouchableOpacity
-              style={[styles.option, styles.dangerOption]}
-              // onPress={handleDelete}
-            >
-              <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
+              <Text style={[styles.optionText, styles.dangerText]}>Delete group chat</Text>
               <TrashIcon fill={Colors.RED} width={18} height={18} />
             </TouchableOpacity>
           </View>
         </View>
       </ScrollView>
 
-      {/* <WarningModal
-        type={'confirm'}
+      <WarningModal
+        type={'delete'}
         isVisible={modalState.isWarningVisible}
-        message={`Are you sure you want to unfriend ${data.user_data.first_name} ${data.user_data.last_name}?`}
-        action={handleUpdateFriendStatus}
-        onClose={() => closeModal('isWarningVisible')}
-        title=""
-      /> */}
+        buttonTitle={modalState.buttonTitle}
+        message={modalState.message}
+        action={modalState.action}
+        onClose={() => setModalState({ ...modalState, isWarningVisible: false })}
+        title={modalState.title}
+      />
       <ImageView
         images={[{ uri: API_HOST + data.settings.avatar_full }]}
         keyExtractor={(imageSrc, index) => index.toString()}
@@ -218,6 +326,7 @@ const GroupSettingScreen: FC<Props> = ({ navigation, route }) => {
         backgroundColor={Colors.DARK_BLUE}
         doubleTapToZoomEnabled={true}
       />
+      <EditGroupModal />
     </PageWrapper>
   );
 };

+ 34 - 6
src/screens/InAppScreens/MessagesScreen/index.tsx

@@ -41,7 +41,7 @@ import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 import GroupIcon from 'assets/icons/messages/group-chat.svg';
 
-const TypingIndicator = () => {
+const TypingIndicator = ({ name }: { name?: string }) => {
   const [dots, setDots] = useState('');
 
   useEffect(() => {
@@ -57,7 +57,13 @@ const TypingIndicator = () => {
     return () => clearInterval(interval);
   }, []);
 
-  return <Text style={styles.typingText}>Typing{dots}</Text>;
+  return name ? (
+    <Text style={styles.typingText}>
+      {name} is typing{dots}
+    </Text>
+  ) : (
+    <Text style={styles.typingText}>Typing{dots}</Text>
+  );
 };
 
 const MessagesScreen = () => {
@@ -71,6 +77,8 @@ const MessagesScreen = () => {
   const [blocked, setBlocked] = useState<Blocked[]>([]);
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
+  const currentUserId = storage.get('uid', StoreType.STRING) as string;
+
   const [filteredChats, setFilteredChats] = useState<{
     all: Chat[];
     unread: Chat[];
@@ -80,7 +88,9 @@ const MessagesScreen = () => {
   const [search, setSearch] = useState('');
   const openRowRef = useRef<any>(null);
   const { isWarningModalVisible, setIsWarningModalVisible } = useChatStore();
-  const [typingUsers, setTypingUsers] = useState<{ [key: string]: boolean }>({});
+  const [typingUsers, setTypingUsers] = useState<
+    { [key: string]: boolean } | { [key: string]: { firstName: string; isTyping: boolean } }
+  >({});
 
   const appState = useRef(AppState.currentState);
 
@@ -167,6 +177,14 @@ const MessagesScreen = () => {
             ...prev,
             [data.conversation_with]: true
           }));
+        } else if (data.group_token) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.group_token]: {
+              isTyping: true,
+              firstName: data.name?.split(' ')[0]
+            }
+          }));
         }
         break;
       case 'stopped_typing':
@@ -175,6 +193,11 @@ const MessagesScreen = () => {
             ...prev,
             [data.conversation_with]: false
           }));
+        } else if (data.group_token) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.group_token]: false
+          }));
         }
         break;
       default:
@@ -372,9 +395,10 @@ const MessagesScreen = () => {
                   {item.pin === 1 ? <PinIcon height={12} fill={Colors.DARK_BLUE} /> : null}
                   {item.muted === 1 ? <BellSlashIcon height={12} fill={Colors.DARK_BLUE} /> : null}
 
-                  {item.sent_by !== item.uid && item.status === 3 ? (
+                  {item.sent_by === +currentUserId && item.status === 3 ? (
                     <ReadIcon fill={Colors.DARK_BLUE} />
-                  ) : item.sent_by !== item.uid && (item.status === 2 || item.status === 1) ? (
+                  ) : item.sent_by === +currentUserId &&
+                    (item.status === 2 || item.status === 1) ? (
                     <UnreadIcon fill={Colors.LIGHT_GRAY} />
                   ) : null}
                   <Text style={styles.chatTime}>{formatDate(item.updated)}</Text>
@@ -382,8 +406,12 @@ const MessagesScreen = () => {
               </View>
 
               <View style={[styles.rowContainer, { flex: 1, gap: 6 }]}>
-                {typingUsers[item.uid ? item.uid : (item.group_chat_token ?? '')] ? (
+                {item.uid && typingUsers[item.uid] ? (
                   <TypingIndicator />
+                ) : item.group_chat_token &&
+                  typingUsers[item.group_chat_token] &&
+                  (typingUsers[item.group_chat_token] as any)?.firstName ? (
+                  <TypingIndicator name={(typingUsers[item.group_chat_token] as any).firstName} />
                 ) : (
                   <Text numberOfLines={2} style={styles.chatMessage}>
                     {item.attachement_name && item.attachement_name.length

+ 8 - 2
src/types/api.ts

@@ -177,7 +177,10 @@ export enum API_ENDPOINT {
   SET_GROUP_ADMIN = 'set-group-admin',
   REMOVE_FROM_GROUP = 'remove-from-group',
   GET_GROUP_SETTINGS = 'get-group-settings',
-  GET_GROUP_MEMBERS = 'get-group-members'
+  GET_GROUP_MEMBERS = 'get-group-members',
+  UPDATE_GROUP_SETTINGS = 'update-group-settings',
+  GET_GROUP_MESSAGE_STATUS = 'get-group-message-status',
+  REMOVE_GROUP_FROM_LIST = 'remove-group-chat-from-conversation-list'
 }
 
 export enum API {
@@ -329,7 +332,10 @@ export enum API {
   SET_GROUP_ADMIN = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_GROUP_ADMIN}`,
   REMOVE_FROM_GROUP = `${API_ROUTE.CHAT}/${API_ENDPOINT.REMOVE_FROM_GROUP}`,
   GET_GROUP_SETTINGS = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_GROUP_SETTINGS}`,
-  GET_GROUP_MEMBERS = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_GROUP_MEMBERS}`
+  GET_GROUP_MEMBERS = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_GROUP_MEMBERS}`,
+  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}`
 }
 
 export type BaseAxiosError = AxiosError;