Pārlūkot izejas kodu

gifted-chat patch + expo-av fixes

Viktoriia 1 nedēļu atpakaļ
vecāks
revīzija
37aea89af2

+ 8 - 1
app.config.ts

@@ -188,11 +188,18 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     ],
     'expo-font',
     [
-      'expo-av',
+      'expo-audio',
       {
         microphonePermission: 'Allow Nomadmania to access your microphone.'
       }
     ],
+    [
+      'expo-video',
+      {
+        supportsBackgroundPlayback: true,
+        supportsPictureInPicture: true
+      }
+    ],
     ['@maplibre/maplibre-react-native'],
     [
       'expo-location',

+ 3 - 1
package.json

@@ -12,6 +12,7 @@
     "postinstall": "patch-package"
   },
   "dependencies": {
+    "@expo/vector-icons": "^15.0.2",
     "@maplibre/maplibre-react-native": "^10.2.1",
     "@react-native-clipboard/clipboard": "^1.16.3",
     "@react-native-community/datetimepicker": "^8.4.4",
@@ -33,7 +34,7 @@
     "dotenv": "^16.6.1",
     "expo": "^54.0.9",
     "expo-asset": "~12.0.9",
-    "expo-av": "~16.0.7",
+    "expo-audio": "~1.0.13",
     "expo-blur": "~15.0.7",
     "expo-build-properties": "~1.0.9",
     "expo-checkbox": "~5.0.7",
@@ -51,6 +52,7 @@
     "expo-status-bar": "~3.0.8",
     "expo-task-manager": "~14.0.7",
     "expo-updates": "~29.0.11",
+    "expo-video": "~3.0.11",
     "formik": "^2.4.6",
     "moment": "^2.30.1",
     "patch-package": "^8.0.0",

+ 59 - 0
patches/react-native-gifted-chat+2.8.1.patch

@@ -0,0 +1,59 @@
+diff --git a/node_modules/react-native-gifted-chat/lib/Composer.js b/node_modules/react-native-gifted-chat/lib/Composer.js
+index becd702..02ed162 100644
+--- a/node_modules/react-native-gifted-chat/lib/Composer.js
++++ b/node_modules/react-native-gifted-chat/lib/Composer.js
+@@ -3,7 +3,7 @@ import { Platform, StyleSheet, TextInput, } from 'react-native';
+ import { MIN_COMPOSER_HEIGHT, DEFAULT_PLACEHOLDER } from './Constant';
+ import Color from './Color';
+ import stylesCommon from './styles';
+-export function Composer({ composerHeight = MIN_COMPOSER_HEIGHT, disableComposer = false, keyboardAppearance = 'default', multiline = true, onInputSizeChanged, onTextChanged, placeholder = DEFAULT_PLACEHOLDER, placeholderTextColor = Color.defaultColor, text = '', textInputAutoFocus = false, textInputProps, textInputStyle, }) {
++export function Composer({ composerHeight = MIN_COMPOSER_HEIGHT, disableComposer = false, keyboardAppearance = 'default', multiline = true, onInputSizeChanged, onTextChanged, placeholder = DEFAULT_PLACEHOLDER, placeholderTextColor = Color.defaultColor, text = '', textInputAutoFocus = false, textInputProps, ref, textInputStyle, }) {
+     const dimensionsRef = useRef(null);
+     const determineInputSizeChange = useCallback((dimensions) => {
+         // Support earlier versions of React Native on Android.
+@@ -32,7 +32,7 @@ export function Composer({ composerHeight = MIN_COMPOSER_HEIGHT, disableComposer
+                     },
+                 }),
+             },
+-        ]} autoFocus={textInputAutoFocus} value={text} enablesReturnKeyAutomatically underlineColorAndroid='transparent' keyboardAppearance={keyboardAppearance} {...textInputProps}/>);
++        ]} autoFocus={textInputAutoFocus} value={text} enablesReturnKeyAutomatically underlineColorAndroid='transparent' keyboardAppearance={keyboardAppearance} ref={ref}/>);
+ }
+ const styles = StyleSheet.create({
+     textInput: {
+diff --git a/node_modules/react-native-gifted-chat/lib/GiftedChat/index.js b/node_modules/react-native-gifted-chat/lib/GiftedChat/index.js
+index 4fc42d2..cd571f8 100644
+--- a/node_modules/react-native-gifted-chat/lib/GiftedChat/index.js
++++ b/node_modules/react-native-gifted-chat/lib/GiftedChat/index.js
+@@ -6,6 +6,7 @@ import { Platform, View, } from 'react-native';
+ import { Actions } from '../Actions';
+ import { Avatar } from '../Avatar';
+ import Bubble from '../Bubble';
++import {Platform as LocalPlatform} from 'react-native'
+ import { Composer } from '../Composer';
+ import { MAX_COMPOSER_HEIGHT, MIN_COMPOSER_HEIGHT, TEST_ID } from '../Constant';
+ import { Day } from '../Day';
+@@ -195,9 +196,10 @@ function GiftedChat(props) {
+             onSend: _onSend,
+             onInputSizeChanged,
+             onTextChanged: _onInputTextChanged,
++            ref: textInputRef,
+             textInputProps: {
+                 ...textInputProps,
+-                ref: textInputRef,
++                // ref: textInputRef,
+                 maxLength: isTypingDisabled ? 0 : maxInputLength,
+             },
+         };
+@@ -279,9 +281,9 @@ function GiftedChat(props) {
+     </GiftedChatContext.Provider>);
+ }
+ function GiftedChatWrapper(props) {
+-    return (<KeyboardProvider>
+-      <GiftedChat {...props}/>
+-    </KeyboardProvider>);
++      return LocalPlatform.OS=='ios'? ( <KeyboardProvider>
++               <GiftedChat {...props}/>
++                </KeyboardProvider>):   <GiftedChat {...props}/>;
+ }
+ GiftedChatWrapper.append = (currentMessages = [], messages, inverted = true) => {
+     if (!Array.isArray(messages))

+ 49 - 30
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -13,7 +13,8 @@ import {
   AppState,
   AppStateStatus,
   TextInput,
-  Platform
+  Platform,
+  Keyboard
 } from 'react-native';
 import {
   GiftedChat,
@@ -32,7 +33,7 @@ import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler'
 import { AvatarWithInitials, Header, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
-import { Audio } from 'expo-av';
+import { setAudioModeAsync } from 'expo-audio';
 import ChatMessageBox from '../Components/ChatMessageBox';
 import ReplyMessageBar from '../Components/ReplyMessageBar';
 import { useSharedValue, withTiming } from 'react-native-reanimated';
@@ -70,7 +71,7 @@ import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
 import { dismissChatNotifications, isMessageEdited } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
-// import FileViewer from 'react-native-file-viewer';
+import FileViewer from 'react-native-file-viewer';
 import * as FileSystem from 'expo-file-system/legacy';
 import ImageView from 'better-react-native-image-viewing';
 import * as MediaLibrary from 'expo-media-library';
@@ -83,6 +84,7 @@ import MessageLocation from '../Components/MessageLocation';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import { useConnection } from 'src/contexts/ConnectionContext';
 import moment from 'moment';
+import CustomComposer from '../CustomComposer';
 
 const options = {
   enableVibrateFallback: true,
@@ -202,19 +204,34 @@ const ChatScreen = ({ route }: { route: any }) => {
   };
 
   useEffect(() => {
-    if (!Audio || !Audio.setAudioModeAsync) {
-      return;
-    }
-
-    Audio.setAudioModeAsync({
-      allowsRecordingIOS: false,
-      staysActiveInBackground: false,
-      playsInSilentModeIOS: true,
-      shouldDuckAndroid: true,
-      playThroughEarpieceAndroid: false
+    setAudioModeAsync({
+      allowsRecording: false,
+      playsInSilentMode: true,
+      shouldPlayInBackground: true,
+      shouldRouteThroughEarpiece: true,
+      interruptionModeAndroid: 'duckOthers',
+      interruptionMode: 'mixWithOthers'
     });
   }, []);
 
+  const [isKeyboardVisible, setKeyboardVisible] = useState(false);
+  
+  useEffect(() => {
+    const keyboardWillShow = Keyboard.addListener(
+      Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
+      () => setKeyboardVisible(true)
+    );
+    const keyboardWillHide = Keyboard.addListener(
+      Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
+      () => setKeyboardVisible(false)
+    );
+
+    return () => {
+      keyboardWillShow.remove();
+      keyboardWillHide.remove();
+    };
+  }, []);
+
   useEffect(() => {
     if (canSendMessage && canSendMessage.need_authentication_or_friend === 1) {
       setModalInfo({
@@ -513,10 +530,10 @@ const ChatScreen = ({ route }: { route: any }) => {
 
       const fileExists = await FileSystem.getInfoAsync(fileUri);
       if (fileExists.exists && fileExists.size > 1024) {
-        // await FileViewer.open(fileUri, {
-        //   showOpenWithDialog: true,
-        //   showAppsSuggestions: true
-        // });
+        await FileViewer.open(fileUri, {
+          showOpenWithDialog: true,
+          showAppsSuggestions: true
+        });
 
         return;
       }
@@ -525,10 +542,10 @@ const ChatScreen = ({ route }: { route: any }) => {
         headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
       });
 
-      // await FileViewer.open(localUri, {
-      //   showOpenWithDialog: true,
-      //   showAppsSuggestions: true
-      // });
+      await FileViewer.open(localUri, {
+        showOpenWithDialog: true,
+        showAppsSuggestions: true
+      });
     } catch (err) {
       console.warn('openFileInApp error:', err);
       Alert.alert('Cannot open file', 'No application found to open this file.');
@@ -2001,10 +2018,10 @@ const ChatScreen = ({ route }: { route: any }) => {
             renderInputToolbar={renderInputToolbar}
             renderCustomView={renderReplyMessageView}
             isCustomViewBottom={false}
-            messageContainerRef={messageContainerRef}
+            messageContainerRef={messageContainerRef as any}
             minComposerHeight={34}
             onInputTextChanged={onInputTextChanged}
-            textInputRef={textInputRef}
+            textInputRef={textInputRef as any}
             isTyping={isTyping}
             renderSend={(props) =>
               editingMessage ? (
@@ -2073,7 +2090,7 @@ const ChatScreen = ({ route }: { route: any }) => {
             )}
             renderAvatar={null}
             maxComposerHeight={100}
-            renderComposer={(props) => <Composer {...props} />}
+            renderComposer={(props) => <Composer {...props} textInputStyle={styles.composer} />}
             keyboardShouldPersistTaps="handled"
             renderChatEmpty={() => (
               <View style={styles.emptyChat}>
@@ -2178,12 +2195,14 @@ const ChatScreen = ({ route }: { route: any }) => {
         <AttachmentsModal />
         <ReactionsListModal />
       </GestureHandlerRootView>
-      <View
-        style={{
-          height: insets.bottom,
-          backgroundColor: Colors.FILL_LIGHT
-        }}
-      />
+      {!isKeyboardVisible ? (
+        <View
+          style={{
+            height: insets.bottom,
+            backgroundColor: Colors.FILL_LIGHT
+          }}
+        />
+      ) : null}
     </SafeAreaView>
   );
 };

+ 26 - 31
src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx

@@ -7,7 +7,7 @@ import { WarningProps } from '../types';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 import { usePostReportConversationMutation } from '@api/chat';
 import * as ImagePicker from 'expo-image-picker';
-// import * as DocumentPicker from 'react-native-document-picker';
+import * as DocumentPicker from '@react-native-documents/picker';
 
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import RouteB from './RouteB';
@@ -126,32 +126,29 @@ const AttachmentsModal = () => {
     if (!chatData) return;
 
     try {
-      // const res = await DocumentPicker.pick({
-      //   type: [DocumentPicker.types.allFiles],
-      //   allowMultiSelection: false
-      // });
-
-      // let file = {
-      //   uri: res[0].uri,
-      //   name: res[0].name,
-      //   type: res[0].type
-      // };
-
-      // if ((file.name && !file.name.includes('.')) || !file.type) {
-      //   file = {
-      //     ...file,
-      //     type: file.type || 'application/octet-stream'
-      //   };
-      // }
-
-      // if (chatData.onSendFile) {
-      //   chatData.onSendFile([file]);
-      // }
+      const res = await DocumentPicker.pick({
+        type: [DocumentPicker.types.allFiles],
+        allowMultiSelection: false
+      });
+
+      let file = {
+        uri: res[0].uri,
+        name: res[0].name,
+        type: res[0].type
+      };
+
+      if ((file.name && !file.name.includes('.')) || !file.type) {
+        file = {
+          ...file,
+          type: file.type || 'application/octet-stream'
+        };
+      }
+
+      if (chatData.onSendFile) {
+        chatData.onSendFile([file]);
+      }
     } catch (err) {
-      // if (DocumentPicker.isCancel(err)) {
-      // } else {
-      //   console.warn('DocumentPicker error:', err);
-      // }
+      console.warn('DocumentPicker error:', err);
     }
 
     SheetManager.hide('chat-attachments');
@@ -197,9 +194,8 @@ const AttachmentsModal = () => {
             <Text style={styles.optionLabel}>Live</Text>
           </TouchableOpacity> */}
 
-          {
-            chatDataRef.current?.isGroup ? (
-              <TouchableOpacity
+          {chatDataRef.current?.isGroup ? (
+            <TouchableOpacity
               style={styles.optionItem}
               onPress={() => {
                 router?.navigate('route-c');
@@ -208,8 +204,7 @@ const AttachmentsModal = () => {
               <PollIcon height={36} />
               <Text style={styles.optionLabel}>Poll</Text>
             </TouchableOpacity>
-            ) : null
-          }
+          ) : null}
 
           {!chatDataRef.current?.isGroup ? (
             <TouchableOpacity style={styles.optionItem} onPress={handleReport}>

+ 91 - 70
src/screens/InAppScreens/MessagesScreen/Components/renderMessageVideo.tsx

@@ -1,40 +1,42 @@
-import React, { useState, useEffect, useRef } from 'react';
-import { View, ActivityIndicator, TouchableOpacity, Platform } from 'react-native';
-import { ResizeMode, Video } from 'expo-av';
+import React, { useState, useEffect, useMemo } from 'react';
+import { View, ActivityIndicator, TouchableOpacity, Platform, Image } from 'react-native';
+import { useEvent } from 'expo';
+import { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import * as FileSystem from 'expo-file-system/legacy';
 import { Colors } from 'src/theme';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import { API_HOST, APP_VERSION } from 'src/constants';
 
+type RenderMessageVideoProps = {
+  props: any;
+  token: string;
+  currentUserId: number;
+  onLongPress: (currentMessage: any, props: any) => any;
+};
+
+const MAX_RETRY = 3;
+
 const RenderMessageVideo = ({
   props,
   token,
   currentUserId,
   onLongPress
-}: {
-  props: any;
-  token: string;
-  currentUserId: number;
-  onLongPress: (currentMessage: any, props: any) => any;
-}) => {
+}: RenderMessageVideoProps) => {
   const { currentMessage } = props;
-
   if (!currentMessage?.video) return null;
+
   const leftMessage = currentMessage?.user?._id !== currentUserId;
 
-  const videoRef = useRef<Video>(null);
-  const [isPlaying, setIsPlaying] = useState(false);
-  const [isBuffering, setIsBuffering] = useState(true);
   const [videoUri, setVideoUri] = useState<string | null>(null);
-  const [isVideoLoaded, setIsVideoLoaded] = useState(false);
   const [retryCount, setRetryCount] = useState(0);
-  const MAX_RETRY = 3;
+
+  const [showPoster, setShowPoster] = useState(true);
+  const [hasStarted, setHasStarted] = useState(false);
 
   const downloadVideo = async (videoUrl: string) => {
     if (!videoUrl.startsWith('https')) {
       setVideoUri(videoUrl);
-      setIsVideoLoaded(true);
       return videoUrl;
     }
 
@@ -44,11 +46,13 @@ const RenderMessageVideo = ({
         await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
       }
 
-      const videoPath = `${CACHED_ATTACHMENTS_DIR}${currentMessage.attachment.filename}`;
+      const filename =
+        currentMessage?.attachment?.filename ?? `video-${currentMessage?._id ?? ''}.mp4`;
+      const videoPath = `${CACHED_ATTACHMENTS_DIR}${filename}`;
 
       const videoExists = await FileSystem.getInfoAsync(videoPath);
       if (videoExists.exists) {
-        if (videoExists.size < 1024) {
+        if ((videoExists.size ?? 0) < 1024) {
           await FileSystem.deleteAsync(videoPath, { idempotent: true });
         } else {
           try {
@@ -56,9 +60,8 @@ const RenderMessageVideo = ({
               encoding: FileSystem.EncodingType.Base64
             });
             setVideoUri(videoPath);
-            setIsVideoLoaded(true);
             return videoPath;
-          } catch (e) {
+          } catch {
             await FileSystem.deleteAsync(videoPath, { idempotent: true });
           }
         }
@@ -73,8 +76,6 @@ const RenderMessageVideo = ({
       });
 
       setVideoUri(downloadResult.uri);
-      setIsVideoLoaded(true);
-
       return downloadResult.uri;
     } catch (error) {
       console.error('Error downloading video:', error);
@@ -85,69 +86,89 @@ const RenderMessageVideo = ({
   useEffect(() => {
     const loadVideo = async () => {
       if (currentMessage?.video && !currentMessage?.isSending) {
+        setShowPoster(true);
         await downloadVideo(currentMessage.video);
       }
     };
-
     loadVideo();
   }, [currentMessage.video, currentMessage.isSending]);
 
-  const handlePlaybackStatusUpdate = (playbackStatus: any) => {
-    if (!playbackStatus.isLoaded) {
-      setIsPlaying(false);
-      setIsBuffering(false);
-      return;
-    }
+  const player = useVideoPlayer(null, (p) => {
+    p.loop = false;
+    p.muted = false;
+    p.volume = 1;
+    p.timeUpdateEventInterval = 0.5;
+  });
 
-    setIsPlaying(playbackStatus.isPlaying);
-    setIsBuffering(playbackStatus.isBuffering ?? false);
-  };
+  useEffect(() => {
+    (async () => {
+      if (videoUri) {
+        const source: VideoSource = { uri: videoUri };
+        await player.replaceAsync(source);
+      }
+    })();
+  }, [videoUri, player]);
+
+  const { status, error } = useEvent(player, 'statusChange', { status: player.status });
+  const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
+
+  const isBuffering = status !== 'readyToPlay';
+  const isVideoLoaded = status === 'readyToPlay';
+
+  useEffect(() => {
+    if (status === 'readyToPlay') setShowPoster(false);
+  }, [status]);
+
+  useEffect(() => {
+    (async () => {
+      if ((status === 'error' || !!error) && retryCount < MAX_RETRY && videoUri) {
+        try {
+          await FileSystem.deleteAsync(videoUri, { idempotent: true });
+        } catch {}
+        const newUri = await downloadVideo(currentMessage.video);
+        if (newUri) {
+          setRetryCount((c) => c + 1);
+          setShowPoster(true);
+        }
+      }
+    })();
+  }, [status, error]);
+
+  const posterUri = useMemo(
+    () =>
+      currentMessage?.attachment?.attachment_small_url
+        ? API_HOST + currentMessage.attachment.attachment_small_url
+        : null,
+    [currentMessage]
+  );
 
   const handlePlayPress = async () => {
-    if (videoRef.current && isVideoLoaded) {
-      await videoRef.current.presentFullscreenPlayer();
-      await videoRef.current.playAsync();
+    if (isVideoLoaded) {
+      setHasStarted(true);
+      await player.play();
     }
   };
 
   return (
-    <View
-      style={{
-        width: 200,
-        height: 200,
-        padding: 6,
-        borderRadius: 10
-      }}
-    >
+    <View style={{ width: 200, height: 200, padding: 6, borderRadius: 10, overflow: 'hidden' }}>
+      {posterUri && showPoster && (
+        <Image
+          source={{ uri: posterUri }}
+          style={{ position: 'absolute', top: 6, left: 6, right: 6, bottom: 6, borderRadius: 10 }}
+          resizeMode="cover"
+        />
+      )}
+
       {videoUri ? (
-        <Video
-          ref={videoRef}
-          source={{ uri: videoUri }}
+        <VideoView
           style={{ flex: 1, borderRadius: 10 }}
-          resizeMode={ResizeMode.CONTAIN}
-          useNativeControls
-          isMuted={false}
-          volume={1.0}
-          shouldCorrectPitch
-          onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
-          posterStyle={{ resizeMode: 'cover', width: '100%', height: '100%' }}
-          usePoster={true}
-          posterSource={{ uri: API_HOST + currentMessage.attachment.attachment_small_url }}
-          onError={async () => {
-            if (retryCount >= MAX_RETRY) {
-              return;
-            }
-
-            if (videoUri) {
-              await FileSystem.deleteAsync(videoUri, { idempotent: true });
-
-              const newUri = await downloadVideo(currentMessage.video);
-              if (newUri) {
-                setVideoUri(newUri);
-                setRetryCount(retryCount + 1);
-              }
-            }
+          player={player}
+          contentFit="contain"
+          nativeControls
+          fullscreenOptions={{
+            enable: true
           }}
+          allowsPictureInPicture={true}
         />
       ) : null}
 
@@ -170,7 +191,7 @@ const RenderMessageVideo = ({
         </View>
       )}
 
-      {!isPlaying && !isBuffering && videoUri && (
+      {!hasStarted && !isBuffering && videoUri && (
         <TouchableOpacity
           style={{
             position: 'absolute',

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

@@ -13,7 +13,8 @@ import {
   AppState,
   AppStateStatus,
   TextInput,
-  Platform
+  Platform,
+  Keyboard
 } from 'react-native';
 import {
   GiftedChat,
@@ -36,7 +37,8 @@ import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler'
 import { Header, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
-import { Audio } from 'expo-av';
+// import { Audio } from 'expo-av';
+import { setAudioModeAsync } from 'expo-audio';
 import ChatMessageBox from '../Components/ChatMessageBox';
 import ReplyMessageBar from '../Components/ReplyMessageBar';
 import { useSharedValue, withTiming } from 'react-native-reanimated';
@@ -78,7 +80,7 @@ import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
 import { dismissChatNotifications, isMessageEdited } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
-// import FileViewer from 'react-native-file-viewer';
+import FileViewer from 'react-native-file-viewer';
 import * as FileSystem from 'expo-file-system/legacy';
 import ImageView from 'better-react-native-image-viewing';
 import * as MediaLibrary from 'expo-media-library';
@@ -243,19 +245,34 @@ const GroupChatScreen = ({ route }: { route: any }) => {
   };
 
   useEffect(() => {
-    if (!Audio || !Audio.setAudioModeAsync) {
-      return;
-    }
-
-    Audio.setAudioModeAsync({
-      allowsRecordingIOS: false,
-      staysActiveInBackground: false,
-      playsInSilentModeIOS: true,
-      shouldDuckAndroid: true,
-      playThroughEarpieceAndroid: false
+    setAudioModeAsync({
+      allowsRecording: false,
+      playsInSilentMode: true,
+      shouldPlayInBackground: true,
+      shouldRouteThroughEarpiece: true,
+      interruptionModeAndroid: 'duckOthers',
+      interruptionMode: 'mixWithOthers'
     });
   }, []);
 
+  const [isKeyboardVisible, setKeyboardVisible] = useState(false);
+
+  useEffect(() => {
+    const keyboardWillShow = Keyboard.addListener(
+      Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
+      () => setKeyboardVisible(true)
+    );
+    const keyboardWillHide = Keyboard.addListener(
+      Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
+      () => setKeyboardVisible(false)
+    );
+
+    return () => {
+      keyboardWillShow.remove();
+      keyboardWillHide.remove();
+    };
+  }, []);
+
   useEffect(() => {
     if (pinData && pinData?.message) {
       setPinned(pinData.message);
@@ -528,10 +545,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
       const fileExists = await FileSystem.getInfoAsync(fileUri);
       if (fileExists.exists && fileExists.size > 1024) {
-        // await FileViewer.open(fileUri, {
-        //   showOpenWithDialog: true,
-        //   showAppsSuggestions: true
-        // });
+        await FileViewer.open(fileUri, {
+          showOpenWithDialog: true,
+          showAppsSuggestions: true
+        });
 
         return;
       }
@@ -540,10 +557,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
       });
 
-      // await FileViewer.open(localUri, {
-      //   showOpenWithDialog: true,
-      //   showAppsSuggestions: true
-      // });
+      await FileViewer.open(localUri, {
+        showOpenWithDialog: true,
+        showAppsSuggestions: true
+      });
     } catch (err) {
       console.warn('openFileInApp error:', err);
       Alert.alert('Cannot open file', 'No application found to open this file.');
@@ -2287,10 +2304,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             renderInputToolbar={renderInputToolbar}
             renderCustomView={renderReplyMessageView}
             isCustomViewBottom={false}
-            messageContainerRef={messageContainerRef}
+            messageContainerRef={messageContainerRef as any}
             minComposerHeight={34}
             onInputTextChanged={onInputTextChanged}
-            textInputRef={textInputRef}
+            textInputRef={textInputRef as any}
             isTyping={isTyping ? true : false}
             renderTypingIndicator={() => <TypingIndicator isTyping={isTyping} />}
             renderSend={(props) =>
@@ -2363,7 +2380,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
               />
             )}
             maxComposerHeight={100}
-            renderComposer={(props) => <Composer {...props} />}
+            renderComposer={(props) => <Composer {...props} textInputStyle={styles.composer} />}
             keyboardShouldPersistTaps="handled"
             renderChatEmpty={() => (
               <View style={styles.emptyChat}>
@@ -2496,12 +2513,14 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         <ReactionsListModal />
         <GroupStatusModal />
       </GestureHandlerRootView>
-      <View
-        style={{
-          height: insets.bottom,
-          backgroundColor: insetsColor
-        }}
-      />
+      {!isKeyboardVisible ? (
+        <View
+          style={{
+            height: insets.bottom,
+            backgroundColor: insetsColor
+          }}
+        />
+      ) : null}
     </SafeAreaView>
   );
 };

+ 55 - 55
src/screens/InAppScreens/TravelsScreen/EventScreen/index.tsx

@@ -14,9 +14,9 @@ import {
 import { styles } from './styles';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
 import { Colors } from 'src/theme';
-// import FileViewer from 'react-native-file-viewer';
+import FileViewer from 'react-native-file-viewer';
 import * as FileSystem from 'expo-file-system/legacy';
-// import * as DocumentPicker from 'react-native-document-picker';
+import * as DocumentPicker from '@react-native-documents/picker';
 import * as ImagePicker from 'expo-image-picker';
 
 import { ScrollView } from 'react-native-gesture-handler';
@@ -298,10 +298,10 @@ const EventScreen = ({ route }: { route: any }) => {
 
       const fileExists = await FileSystem.getInfoAsync(fileUri);
       if (fileExists.exists && fileExists.size > 1024) {
-        // await FileViewer.open(fileUri, {
-        //   showOpenWithDialog: true,
-        //   showAppsSuggestions: true
-        // });
+        await FileViewer.open(fileUri, {
+          showOpenWithDialog: true,
+          showAppsSuggestions: true
+        });
 
         return;
       }
@@ -321,10 +321,10 @@ const EventScreen = ({ route }: { route: any }) => {
         headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
       });
 
-      // await FileViewer.open(localUri, {
-      //   showOpenWithDialog: true,
-      //   showAppsSuggestions: true
-      // });
+      await FileViewer.open(localUri, {
+        showOpenWithDialog: true,
+        showAppsSuggestions: true
+      });
     } catch (error) {
       console.error('Error previewing document:', error);
     } finally {
@@ -338,53 +338,53 @@ const EventScreen = ({ route }: { route: any }) => {
 
   const handleUploadFile = useCallback(async () => {
     try {
-      // const response = await DocumentPicker.pick({
-      //   type: [DocumentPicker.types.allFiles],
-      //   allowMultiSelection: true
-      // });
+      const response = await DocumentPicker.pick({
+        type: [DocumentPicker.types.allFiles],
+        allowMultiSelection: true
+      });
 
       setIsUploading(true);
-      // for (const res of response) {
-      //   let file: any = {
-      //     uri: res.uri,
-      //     name: res.name,
-      //     type: res.type
-      //   };
-
-      //   if ((file.name && !file.name.includes('.')) || !file.type) {
-      //     file = {
-      //       ...file,
-      //       type: file.type || 'application/octet-stream'
-      //     };
-      //   }
-
-      //   await uploadTempFile(
-      //     {
-      //       token,
-      //       file,
-      //       onUploadProgress: (progressEvent) => {
-      //         // if (progressEvent.lengthComputable) {
-      //         //   const progress = Math.round(
-      //         //     (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
-      //         //   );
-      //         //   setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
-      //         // }
-      //       }
-      //     },
-      //     {
-      //       onSuccess: (result) => {
-      //         setMyTempFiles((prev) => [
-      //           { ...result, type: 1, description: '', isSending: false },
-      //           ...prev
-      //         ]);
-      //         setIsUploading(false);
-      //       },
-      //       onError: (error) => {
-      //         console.error('Upload error:', error);
-      //       }
-      //     }
-      //   );
-      // }
+      for (const res of response) {
+        let file: any = {
+          uri: res.uri,
+          name: res.name,
+          type: res.type
+        };
+
+        if ((file.name && !file.name.includes('.')) || !file.type) {
+          file = {
+            ...file,
+            type: file.type || 'application/octet-stream'
+          };
+        }
+
+        await uploadTempFile(
+          {
+            token,
+            file,
+            onUploadProgress: (progressEvent) => {
+              // if (progressEvent.lengthComputable) {
+              //   const progress = Math.round(
+              //     (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
+              //   );
+              //   setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
+              // }
+            }
+          },
+          {
+            onSuccess: (result) => {
+              setMyTempFiles((prev) => [
+                { ...result, type: 1, description: '', isSending: false },
+                ...prev
+              ]);
+              setIsUploading(false);
+            },
+            onError: (error) => {
+              console.error('Upload error:', error);
+            }
+          }
+        );
+      }
     } catch {
       setIsUploading(false);
     } finally {

+ 1 - 2
tsconfig.json

@@ -6,6 +6,5 @@
     "paths": {
       "@api/*": ["src/modules/api/*"]
     }
-  },
-  "include": ["src", "declarations.d.ts"]
+  }
 }