Procházet zdrojové kódy

notifications, reset badges count, small fixes

Viktoriia před 8 měsíci
rodič
revize
aebd5b5f75

+ 2 - 0
src/contexts/PushNotificationContext.tsx

@@ -110,6 +110,8 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
         })
       });
 
+      Notifications.setBadgeCountAsync(0);
+
       const notificationListener = Notifications.addNotificationReceivedListener((notification) => {
         console.log('Notification received');
       });

+ 1 - 1
src/modules/api/notifications/notifications-api.ts

@@ -4,7 +4,7 @@ import { ResponseType } from '../response-type';
 
 export interface PostGetSettings {
   settings: {
-    name: 'app-ios' | 'app-android' | 'app-friends' | 'email-friends';
+    name: 'app-ios' | 'app-android' | 'app-friends' | 'email-friends' | 'app-messages';
     active: 0 | 1;
   }[];
 }

+ 194 - 111
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -9,7 +9,9 @@ import {
   Dimensions,
   Alert,
   ScrollView,
-  Linking
+  Linking,
+  Platform,
+  ActivityIndicator
 } from 'react-native';
 import {
   GiftedChat,
@@ -31,7 +33,7 @@ import {
   LongPressGestureHandler,
   Swipeable
 } from 'react-native-gesture-handler';
-import { AvatarWithInitials, Header } from 'src/components';
+import { AvatarWithInitials, Header, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
 import { Video } from 'expo-av';
@@ -62,6 +64,8 @@ import { styles } from './styles';
 import SendIcon from 'assets/icons/messages/send.svg';
 import { SheetManager } from 'react-native-actions-sheet';
 import { NAVIGATION_PAGES } from 'src/types';
+import * as Notifications from 'expo-notifications';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
 
 const options = {
   enableVibrateFallback: true,
@@ -75,7 +79,7 @@ const ChatScreen = ({ route }: { route: any }) => {
   const currentUserId = storage.get('uid', StoreType.STRING) as number;
   const token = storage.get('token', StoreType.STRING) as string;
   const insets = useSafeAreaInsets();
-  const [messages, setMessages] = useState<CustomMessage[]>([]);
+  const [messages, setMessages] = useState<CustomMessage[] | null>();
   const { showActionSheetWithOptions } = useActionSheet();
   const navigation = useNavigation();
   const { data: chatData, isFetching, refetch } = usePostGetChatWithQuery(token, id, -1, -1, true);
@@ -86,6 +90,12 @@ const ChatScreen = ({ route }: { route: any }) => {
   const [selectedMedia, setSelectedMedia] = useState<any>(null);
 
   const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
+  const [modalInfo, setModalInfo] = useState({
+    visible: false,
+    type: 'confirm',
+    message: '',
+    action: () => {}
+  });
 
   const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | null>(null);
   const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false);
@@ -111,9 +121,61 @@ const ChatScreen = ({ route }: { route: any }) => {
   const messageRefs = useRef<{ [key: string]: any }>({});
   const flatList = useRef<FlatList | null>(null);
   const scrollY = useSharedValue(0);
+  const { isSubscribed } = usePushNotification();
 
   const socket = useRef<WebSocket | null>(null);
 
+  const closeModal = () => {
+    setModalInfo({ ...modalInfo, visible: false });
+  };
+
+  const dismissChatNotifications = async (chatWithUserId: number) => {
+    const { status } = await Notifications.getPermissionsAsync();
+    if (status !== 'granted' || !isSubscribed) {
+      setModalInfo({
+        visible: true,
+        type: 'success',
+        message:
+          'To use this feature we need your permission to access your notifications. You will be redirected to the notification settings screen where you need to enable them.',
+        action: () =>
+          // @ts-ignore
+          navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
+            screen: NAVIGATION_PAGES.NOTIFICATIONS
+          })
+      });
+      return;
+    }
+    const subscription = Notifications.addNotificationReceivedListener((notification) => {
+      let conversation_with_user = 0;
+      if (Platform.OS === 'android') {
+        const data = notification.request.content.data;
+        if (data?.params) {
+          const parsedParams = JSON.parse(data.params) ?? {};
+          conversation_with_user = parsedParams?.id;
+        }
+      } else {
+        const data = (notification.request.trigger as Notifications.PushNotificationTrigger)
+          ?.payload;
+        if (data?.params) {
+          const parsedParams = JSON.parse(data.params as string) ?? {};
+          conversation_with_user = parsedParams?.id;
+        }
+      }
+
+      if (conversation_with_user === chatWithUserId) {
+        Notifications.dismissNotificationAsync(notification.request.identifier);
+      }
+    });
+
+    return () => {
+      subscription.remove();
+    };
+  };
+
+  useEffect(() => {
+    dismissChatNotifications(id);
+  }, [id]);
+
   useEffect(() => {
     socket.current = new WebSocket(WEBSOCKET_URL);
 
@@ -347,8 +409,11 @@ const ChatScreen = ({ route }: { route: any }) => {
       },
       {
         onSuccess: () => {
-          setMessages((prevMessages) => prevMessages.filter((msg) => msg._id !== messageId));
+          setMessages((prevMessages) =>
+            prevMessages ? prevMessages.filter((msg) => msg._id !== messageId) : []
+          );
           // sendWebSocketMessage('message_deleted');
+          refetch();
           sendWebSocketMessage('new_message');
         }
       }
@@ -372,10 +437,6 @@ const ChatScreen = ({ route }: { route: any }) => {
         handleDeleteMessage(selectedMessage.currentMessage?._id);
         setIsModalVisible(false);
         break;
-      // case 'pin':
-      //   setIsModalVisible(false);
-      //   Alert.alert(option);
-      //   break;
       default:
         break;
     }
@@ -392,7 +453,7 @@ const ChatScreen = ({ route }: { route: any }) => {
       {
         onSuccess: () => {
           setMessages((prevMessages) =>
-            prevMessages.map((msg) => {
+            prevMessages?.map((msg) => {
               if (msg._id === messageId) {
                 return {
                   ...msg,
@@ -583,7 +644,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         }
       );
 
-      setMessages((previousMessages) => GiftedChat.append(previousMessages, [message]));
+      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
       clearReplyMessage();
     },
     [replyMessage]
@@ -630,7 +691,9 @@ const ChatScreen = ({ route }: { route: any }) => {
         image: result.assets[0].type === 'image' ? result.assets[0].uri : null,
         video: result.assets[0].type === 'video' ? result.assets[0].uri : null
       };
-      setMessages((previousMessages) => GiftedChat.append(previousMessages, [newMedia as any]));
+      setMessages((previousMessages) =>
+        GiftedChat.append(previousMessages ?? [], [newMedia as any])
+      );
     }
   };
 
@@ -658,7 +721,7 @@ const ChatScreen = ({ route }: { route: any }) => {
       }));
 
       setMessages((previousMessages) =>
-        GiftedChat.append(previousMessages, imageMessages as any[])
+        GiftedChat.append(previousMessages ?? [], imageMessages as any[])
       );
     }
   };
@@ -689,6 +752,8 @@ const ChatScreen = ({ route }: { route: any }) => {
   };
 
   const addReaction = (messageId: number, reaction: string) => {
+    if (!messages) return;
+
     const updatedMessages = messages.map((msg: any) => {
       if (msg._id === messageId) {
         const updatedReactions: Reaction[] = [
@@ -783,6 +848,8 @@ const ChatScreen = ({ route }: { route: any }) => {
   };
 
   const scrollToMessage = (messageId: number) => {
+    if (!messages) return;
+
     const messageIndex = messages.findIndex((message) => message._id === messageId);
 
     if (messageIndex !== -1 && flatList.current) {
@@ -933,12 +1000,13 @@ const ChatScreen = ({ route }: { route: any }) => {
   const renderInputToolbar = (props: any) => (
     <InputToolbar
       {...props}
-      renderActions={() => (
-        <Actions
-          icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
-          // onPressActionButton={openActionSheet}
-        />
-      )}
+      renderActions={() =>
+        // <Actions
+        //   icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
+        //   // onPressActionButton={openActionSheet}
+        // />
+        null
+      }
       containerStyle={{
         backgroundColor: Colors.FILL_LIGHT
       }}
@@ -1015,101 +1083,105 @@ const ChatScreen = ({ route }: { route: any }) => {
       </View>
 
       <GestureHandlerRootView style={styles.container}>
-        <GiftedChat
-          messages={messages}
-          listViewProps={{
-            ref: flatList,
-            showsVerticalScrollIndicator: false,
-            initialNumToRender: 30,
-            onViewableItemsChanged: handleViewableItemsChanged,
-            viewabilityConfig: { itemVisiblePercentThreshold: 50 },
-            initialScrollIndex: unreadMessageIndex ?? 0,
-            onScrollToIndexFailed: (info: any) => {
-              const wait = new Promise((resolve) => setTimeout(resolve, 300));
-              wait.then(() => {
-                flatList.current?.scrollToIndex({
-                  index: info.index,
-                  animated: true,
-                  viewPosition: 0.5
+        {messages ? (
+          <GiftedChat
+            messages={messages as CustomMessage[]}
+            listViewProps={{
+              ref: flatList,
+              showsVerticalScrollIndicator: false,
+              initialNumToRender: 30,
+              onViewableItemsChanged: handleViewableItemsChanged,
+              viewabilityConfig: { itemVisiblePercentThreshold: 50 },
+              initialScrollIndex: unreadMessageIndex ?? 0,
+              onScrollToIndexFailed: (info: any) => {
+                const wait = new Promise((resolve) => setTimeout(resolve, 300));
+                wait.then(() => {
+                  flatList.current?.scrollToIndex({
+                    index: info.index,
+                    animated: true,
+                    viewPosition: 0.5
+                  });
                 });
-              });
-            }
-          }}
-          renderSystemMessage={renderSystemMessage}
-          onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
-          user={{ _id: +currentUserId, name: 'Me' }}
-          renderBubble={renderBubble}
-          renderMessageImage={renderMessageImage}
-          renderInputToolbar={renderInputToolbar}
-          renderCustomView={renderReplyMessageView}
-          isCustomViewBottom={false}
-          messageContainerRef={messageContainerRef}
-          minComposerHeight={34}
-          onInputTextChanged={(text) => handleTyping(text.length > 0)}
-          isTyping={isTyping}
-          renderSend={(props) => (
-            <View
-              style={{
-                flexDirection: 'row',
-                height: '100%',
-                alignItems: 'center',
-                justifyContent: 'center',
-                paddingHorizontal: 14
-              }}
-            >
-              {props.text?.trim() && (
-                <Send
-                  {...props}
-                  containerStyle={{
-                    justifyContent: 'center'
-                  }}
-                >
-                  <SendIcon fill={Colors.DARK_BLUE} />
-                </Send>
-              )}
-              {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
-            </View>
-          )}
-          textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }}
-          placeholder=""
-          renderMessage={(props) => (
-            <ChatMessageBox
-              {...(props as MessageProps<CustomMessage>)}
-              updateRowRef={updateRowRef}
-              setReplyOnSwipeOpen={setReplyMessage}
-            />
-          )}
-          renderChatFooter={() => (
-            <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
-          )}
-          // renderMessageVideo={renderMessageVideo}
-          renderAvatar={null}
-          maxComposerHeight={100}
-          renderComposer={(props) => <Composer {...props} />}
-          keyboardShouldPersistTaps="handled"
-          renderChatEmpty={() => (
-            <View style={styles.emptyChat}>
-              <Text
-                style={styles.emptyChatText}
-              >{`No messages yet.\nFeel free to start the conversation.`}</Text>
-            </View>
-          )}
-          shouldUpdateMessage={shouldUpdateMessage}
-          scrollToBottom={true}
-          scrollToBottomComponent={renderScrollToBottom}
-          scrollToBottomStyle={{ backgroundColor: 'transparent' }}
-          parsePatterns={(linkStyle) => [
-            {
-              type: 'url',
-              style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
-              onPress: (url: string) => Linking.openURL(url),
-              onLongPress: (url: string) => {
-                Clipboard.setString(url ?? '');
-                Alert.alert('Link copied');
               }
-            }
-          ]}
-        />
+            }}
+            renderSystemMessage={renderSystemMessage}
+            onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
+            user={{ _id: +currentUserId, name: 'Me' }}
+            renderBubble={renderBubble}
+            renderMessageImage={renderMessageImage}
+            renderInputToolbar={renderInputToolbar}
+            renderCustomView={renderReplyMessageView}
+            isCustomViewBottom={false}
+            messageContainerRef={messageContainerRef}
+            minComposerHeight={34}
+            onInputTextChanged={(text) => handleTyping(text.length > 0)}
+            isTyping={isTyping}
+            renderSend={(props) => (
+              <View
+                style={{
+                  flexDirection: 'row',
+                  height: '100%',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  paddingHorizontal: 14
+                }}
+              >
+                {props.text?.trim() && (
+                  <Send
+                    {...props}
+                    containerStyle={{
+                      justifyContent: 'center'
+                    }}
+                  >
+                    <SendIcon fill={Colors.DARK_BLUE} />
+                  </Send>
+                )}
+                {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
+              </View>
+            )}
+            textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }}
+            placeholder=""
+            renderMessage={(props) => (
+              <ChatMessageBox
+                {...(props as MessageProps<CustomMessage>)}
+                updateRowRef={updateRowRef}
+                setReplyOnSwipeOpen={setReplyMessage}
+              />
+            )}
+            renderChatFooter={() => (
+              <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+            )}
+            // renderMessageVideo={renderMessageVideo}
+            renderAvatar={null}
+            maxComposerHeight={100}
+            renderComposer={(props) => <Composer {...props} />}
+            keyboardShouldPersistTaps="handled"
+            renderChatEmpty={() => (
+              <View style={styles.emptyChat}>
+                <Text
+                  style={styles.emptyChatText}
+                >{`No messages yet.\nFeel free to start the conversation.`}</Text>
+              </View>
+            )}
+            shouldUpdateMessage={shouldUpdateMessage}
+            scrollToBottom={true}
+            scrollToBottomComponent={renderScrollToBottom}
+            scrollToBottomStyle={{ backgroundColor: 'transparent' }}
+            parsePatterns={(linkStyle) => [
+              {
+                type: 'url',
+                style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
+                onPress: (url: string) => Linking.openURL(url),
+                onLongPress: (url: string) => {
+                  Clipboard.setString(url ?? '');
+                  Alert.alert('Link copied');
+                }
+              }
+            ]}
+          />
+        ) : (
+          <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+        )}
 
         <Modal visible={!!selectedMedia} transparent={true}>
           <View style={styles.modalContainer}>
@@ -1169,6 +1241,17 @@ const ChatScreen = ({ route }: { route: any }) => {
             </TouchableOpacity>
           </BlurView>
         </ReactModal>
+
+        <WarningModal
+          isVisible={modalInfo.visible}
+          onClose={closeModal}
+          type={modalInfo.type}
+          message={modalInfo.message}
+          action={() => {
+            modalInfo.action();
+            closeModal();
+          }}
+        />
       </GestureHandlerRootView>
       <View
         style={{

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

@@ -60,7 +60,7 @@ const OptionsMenu: React.FC<OptionsMenuProps> = ({
 const styles = StyleSheet.create({
   optionsMenu: {
     position: 'absolute',
-    backgroundColor: 'rgba(255, 255, 255, 0.9)',
+    backgroundColor: 'rgba(255, 255, 255, 1)',
     borderRadius: 10,
     padding: 8,
     shadowColor: '#000',

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

@@ -61,7 +61,7 @@ const styles = StyleSheet.create({
     position: 'absolute',
     width: Dimensions.get('window').width * 0.75,
     flexDirection: 'row',
-    backgroundColor: 'rgba(255, 255, 255, 0.9)',
+    backgroundColor: 'rgba(255, 255, 255, 1)',
     justifyContent: 'space-between',
     alignItems: 'center',
     borderRadius: 20,

+ 45 - 17
src/screens/NotificationsScreen/index.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { View, Linking, Text, Switch, Platform, TouchableOpacity, AppState } from 'react-native';
 import * as Notifications from 'expo-notifications';
 
@@ -15,7 +15,7 @@ import PeopleIcon from 'assets/icons/notifications/people-group-solid.svg';
 import ChatIcon from 'assets/icons/notifications/messages.svg';
 import GearIcon from 'assets/icons/notifications/gear-solid.svg';
 import BellIcon from 'assets/icons/notifications/bell-solid.svg';
-import { useGetSettingsQuery } from '@api/notifications';
+import { useGetSettingsQuery, usePostSetSettingsMutation } from '@api/notifications';
 import { useFocusEffect } from '@react-navigation/native';
 
 const NotificationsScreen = ({ navigation }: { navigation: any }) => {
@@ -24,6 +24,7 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
 
   const { mutateAsync: saveNotificationToken } = usePostSaveNotificationTokenMutation();
   const { isSubscribed, toggleSubscription, unsubscribeFromNotifications } = usePushNotification();
+  const { mutateAsync: setNotificationsSettings } = usePostSetSettingsMutation();
   const [modalInfo, setModalInfo] = useState({
     visible: false,
     type: 'confirm',
@@ -34,6 +35,7 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
   const [initialPermissionStatus, setInitialPermissionStatus] = useState<
     'granted' | 'denied' | 'undetermined' | null
   >(null);
+  const [isEnabledMessages, setIsEnabledMessages] = useState(false);
 
   const closeModal = () => {
     setModalInfo({ ...modalInfo, visible: false });
@@ -42,13 +44,9 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
   useEffect(() => {
     if (notificationsSettings) {
       const { settings } = notificationsSettings;
-      const isServerSubscribed =
-        Platform.OS === 'ios'
-          ? settings.some((setting) => setting.name === 'app-ios' && setting.active === 1)
-          : settings.some((setting) => setting.name === 'app-android' && setting.active === 1);
-      // if (isServerSubscribed !== isSubscribed) {
-      //   toggleSubscription();
-      // }
+      setIsEnabledMessages(
+        settings.some((setting) => setting.name === 'app-messages' && setting.active === 1)
+      );
     }
   }, [notificationsSettings]);
 
@@ -168,6 +166,17 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
     return { notificationToken: deviceData.data ?? '', platform: deviceData.type ?? '' };
   }
 
+  const handleToggleMessages = async () => {
+    const dataToUpdate = { 'app-messages': isEnabledMessages ? 0 : 1 };
+    setIsEnabledMessages(!isEnabledMessages);
+
+    await setNotificationsSettings({
+      token,
+      settings: JSON.stringify(dataToUpdate)
+    });
+    refetchData();
+  };
+
   return (
     <PageWrapper>
       <Header label="Notifications" />
@@ -229,19 +238,38 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
         disabled={!isSubscribed}
       />
 
-      {/* <MenuButton
-        label="Messages"
-        icon={
+      <TouchableOpacity
+        style={[
+          styles.alignStyle,
+          styles.buttonWrapper,
+          {
+            justifyContent: 'space-between'
+          }
+        ]}
+        onPress={handleToggleMessages}
+        disabled={!isSubscribed}
+      >
+        <View style={styles.alignStyle}>
           <ChatIcon
             fill={isSubscribed ? Colors.DARK_BLUE : Colors.LIGHT_GRAY}
             width={20}
             height={20}
           />
-        }
-        red={false}
-        buttonFn={() => navigation.navigate(NAVIGATION_PAGES.MESSAGES_NOTIFICATIONS as never)}
-        disabled={!isSubscribed}
-      /> */}
+          <Text style={[styles.buttonLabel, !isSubscribed ? { color: Colors.LIGHT_GRAY } : {}]}>
+            Messages
+          </Text>
+        </View>
+        <View>
+          <Switch
+            trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+            thumbColor={Colors.WHITE}
+            onValueChange={handleToggleMessages}
+            value={isEnabledMessages && isSubscribed}
+            style={{ transform: 'scale(0.8)' }}
+            disabled={!isSubscribed}
+          />
+        </View>
+      </TouchableOpacity>
 
       {/* <MenuButton
         label="System"