diff --git a/components/pages/settings/user_settings_views/MyGroup.tsx b/components/pages/settings/user_settings_views/MyGroup.tsx index 7164082..0b69514 100644 --- a/components/pages/settings/user_settings_views/MyGroup.tsx +++ b/components/pages/settings/user_settings_views/MyGroup.tsx @@ -174,7 +174,7 @@ const MyGroup = () => { padding-10 > @@ -213,7 +213,7 @@ const MyGroup = () => { padding-10 > diff --git a/components/pages/settings/user_settings_views/MyProfile.tsx b/components/pages/settings/user_settings_views/MyProfile.tsx index fe91d08..1d90b0a 100644 --- a/components/pages/settings/user_settings_views/MyProfile.tsx +++ b/components/pages/settings/user_settings_views/MyProfile.tsx @@ -1,30 +1,34 @@ -import {Colors, Picker, Text, TextField, View} from "react-native-ui-lib"; import React, {useEffect, useRef, useState} from "react"; -import {ImageBackground, StyleSheet} from "react-native"; +import {StyleSheet, TouchableOpacity} from "react-native"; import {ScrollView} from "react-native-gesture-handler"; +import * as ImagePicker from "expo-image-picker"; +import {Colors, Image, Picker, Text, TextField, View} from "react-native-ui-lib"; +import Ionicons from "@expo/vector-icons/Ionicons"; +import * as tz from "tzdata"; +import * as Localization from "expo-localization"; +import debounce from "debounce"; import {useAuthContext} from "@/contexts/AuthContext"; import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData"; -import Ionicons from "@expo/vector-icons/Ionicons"; -import * as tz from 'tzdata'; -import * as Localization from 'expo-localization'; -import debounce from "debounce"; +import {useChangeProfilePicture} from "@/hooks/firebase/useChangeProfilePicture"; const MyProfile = () => { const {user, profileData} = useAuthContext(); - - const [timeZone, setTimeZone] = useState(profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone); + const [timeZone, setTimeZone] = useState( + profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone + ); const [lastName, setLastName] = useState(profileData?.lastName || ""); const [firstName, setFirstName] = useState( profileData?.firstName || "" ); + const [profileImage, setProfileImage] = useState(profileData?.pfp || null); const {mutateAsync: updateUserData} = useUpdateUserData(); + const {mutateAsync: changeProfilePicture} = useChangeProfilePicture(); const isFirstRender = useRef(true); - const handleUpdateUserData = async () => { await updateUserData({newUserData: {firstName, lastName, timeZone}}); - } + }; const debouncedUserDataUpdate = debounce(handleUpdateUserData, 500); @@ -34,22 +38,68 @@ const MyProfile = () => { return; } debouncedUserDataUpdate(); - }, [timeZone, lastName, firstName]); + }, [timeZone, lastName, firstName, profileImage]); + + useEffect(() => { + if (profileData) { + setFirstName(profileData.firstName || ""); + setLastName(profileData.lastName || ""); + // setProfileImage(profileData.pfp || null); + setTimeZone(profileData.timeZone || Localization.getCalendars()[0].timeZone!); + } + }, [profileData]); + + const pickImage = async () => { + const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!permissionResult.granted) { + alert("Permission to access camera roll is required!"); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 1, + }); + + if (!result.canceled) { + setProfileImage(result.assets[0].uri); + changeProfilePicture(result.assets[0]) + } + }; + + const handleClearImage = async () => { + await updateUserData({newUserData: {pfp: null}}); + setProfileImage(null) + } + + const pfpUri = profileImage && typeof profileImage === 'object' && 'uri' in profileImage ? profileImage.uri : profileImage; return ( Your Profile - + + + - - Change Photo - - Remove Photo + + + {profileData?.pfp ? "Change" : "Add"} Photo + + + + {profileData?.pfp && ( + + Remove Photo + + )} @@ -94,24 +144,27 @@ const MyProfile = () => { Time Zone { - setTimeZone(item as string) - }} + onChange={(item) => setTimeZone(item as string)} showSearch floatingPlaceholder style={styles.inViewPicker} trailingAccessory={ - - + + } > @@ -123,9 +176,11 @@ const MyProfile = () => { ); }; -const timeZoneItems = Object.keys(tz.zones).sort().map((zone) => ( - -)); +const timeZoneItems = Object.keys(tz.zones) + .sort() + .map((zone) => ( + + )); const styles = StyleSheet.create({ card: { @@ -139,7 +194,7 @@ const styles = StyleSheet.create({ pfp: { aspectRatio: 1, width: 65.54, - backgroundColor: "green", + backgroundColor: "gray", borderRadius: 20, }, txtBox: { @@ -150,7 +205,7 @@ const styles = StyleSheet.create({ padding: 15, height: 45, fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13 + fontSize: 13, }, subTit: { fontFamily: "Manrope_500Medium", @@ -159,11 +214,11 @@ const styles = StyleSheet.create({ label: { fontFamily: "PlusJakartaSans_500Medium", fontSize: 12, - color: "#a1a1a1" + color: "#a1a1a1", }, photoSet: { fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13.07 + fontSize: 13.07, }, jakarta12: { paddingVertical: 10, @@ -171,18 +226,6 @@ const styles = StyleSheet.create({ fontSize: 12, color: "#a1a1a1", }, - picker: { - borderRadius: 50, - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: Colors.grey80, - marginBottom: 16, - borderColor: Colors.grey50, - borderWidth: 1, - marginTop: -20, - height: 40, - zIndex: 10, - }, viewPicker: { borderRadius: 50, backgroundColor: Colors.grey80, @@ -204,4 +247,4 @@ const styles = StyleSheet.create({ }, }); -export default MyProfile; +export default MyProfile; \ No newline at end of file diff --git a/components/shared/AssigneesDisplay.tsx b/components/shared/AssigneesDisplay.tsx index 92b6317..90343ab 100644 --- a/components/shared/AssigneesDisplay.tsx +++ b/components/shared/AssigneesDisplay.tsx @@ -1,6 +1,6 @@ import React from "react"; -import {ImageBackground, StyleSheet} from "react-native"; -import {Text, TouchableOpacity, View} from "react-native-ui-lib"; +import {StyleSheet} from "react-native"; +import {Image, Text, TouchableOpacity, View} from "react-native-ui-lib"; import RemoveAssigneeBtn from "./RemoveAssigneeBtn"; import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers"; @@ -26,7 +26,7 @@ const AssigneesDisplay = ({selectedAttendees, setSelectedAttendees}: { removeAttendee(member.uid!)}> {member?.pfp ? ( - } diff --git a/hooks/firebase/types/profileTypes.ts b/hooks/firebase/types/profileTypes.ts index 96ecac9..3179f65 100644 --- a/hooks/firebase/types/profileTypes.ts +++ b/hooks/firebase/types/profileTypes.ts @@ -17,7 +17,7 @@ export interface UserProfile { password: string; familyId?: string; uid?: string; - pfp?: string; + pfp?: string | null; eventColor?: string | null; timeZone?: string | null; firstDayOfWeek?: string | null; diff --git a/hooks/firebase/useChangeProfilePicture.ts b/hooks/firebase/useChangeProfilePicture.ts new file mode 100644 index 0000000..4f0b97e --- /dev/null +++ b/hooks/firebase/useChangeProfilePicture.ts @@ -0,0 +1,57 @@ +import { useMutation, useQueryClient } from "react-query"; +import firestore from "@react-native-firebase/firestore"; +import storage from "@react-native-firebase/storage"; +import { useAuthContext } from "@/contexts/AuthContext"; +import * as ImagePicker from "expo-image-picker"; +import { Platform } from "react-native"; + +export const useChangeProfilePicture = () => { + const queryClient = useQueryClient(); + const { user, refreshProfileData } = useAuthContext(); + + return useMutation({ + mutationKey: ["changeProfilePicture"], + mutationFn: async (profilePicture: ImagePicker.ImagePickerAsset) => { + if (!profilePicture?.uri) { + throw new Error("No image selected"); + } + + let imageUri = profilePicture.uri; + + console.log("Selected image URI:", imageUri); + + if (Platform.OS === 'ios' && !imageUri.startsWith('file://')) { + imageUri = `file://${imageUri}`; + console.log("Updated image URI for iOS:", imageUri); + } + + const fileName = `profilePictures/${new Date().getTime()}_profile.jpg`; + console.log("Firebase Storage file path:", fileName); + + try { + const reference = storage().ref(fileName); + + console.log('Uploading image to Firebase Storage...'); + await reference.putFile(imageUri); + console.log('Image uploaded successfully!'); + + const downloadURL = await reference.getDownloadURL(); + console.log("Download URL:", downloadURL); + + await firestore() + .collection("Profiles") + .doc(user?.uid) + .update({ pfp: downloadURL }); + + } catch (e) { + console.error("Error uploading profile picture:", e.message); + throw e; + } + }, + onSuccess: () => { + // Invalidate queries to refresh profile data + queryClient.invalidateQueries("Profiles"); + refreshProfileData(); + }, + }); +}; \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f8b463d..b467d5a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1311,6 +1311,9 @@ PODS: - Firebase/Functions (10.29.0): - Firebase/CoreOnly - FirebaseFunctions (~> 10.29.0) + - Firebase/Storage (10.29.0): + - Firebase/CoreOnly + - FirebaseStorage (~> 10.29.0) - FirebaseAppCheckInterop (10.29.0) - FirebaseAuth (10.29.0): - FirebaseAppCheckInterop (~> 10.17) @@ -1382,6 +1385,13 @@ PODS: - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - FirebaseSharedSwift (10.29.0) + - FirebaseStorage (10.29.0): + - FirebaseAppCheckInterop (~> 10.0) + - FirebaseAuthInterop (~> 10.25) + - FirebaseCore (~> 10.0) + - FirebaseCoreExtension (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GTMSessionFetcher/Core (< 4.0, >= 2.1) - fmt (9.1.0) - glog (0.3.5) - GoogleDataTransport (9.4.1): @@ -2719,6 +2729,10 @@ PODS: - Firebase/Functions (= 10.29.0) - React-Core - RNFBApp + - RNFBStorage (21.0.0): + - Firebase/Storage (= 10.29.0) + - React-Core + - RNFBApp - RNGestureHandler (2.16.2): - DoubleConversion - glog @@ -2897,6 +2911,7 @@ DEPENDENCIES: - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" - "RNFBFirestore (from `../node_modules/@react-native-firebase/firestore`)" - "RNFBFunctions (from `../node_modules/@react-native-firebase/functions`)" + - "RNFBStorage (from `../node_modules/@react-native-firebase/storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) @@ -2924,6 +2939,7 @@ SPEC REPOS: - FirebaseRemoteConfigInterop - FirebaseSessions - FirebaseSharedSwift + - FirebaseStorage - GoogleDataTransport - GoogleUtilities - "gRPC-C++" @@ -3138,6 +3154,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/firestore" RNFBFunctions: :path: "../node_modules/@react-native-firebase/functions" + RNFBStorage: + :path: "../node_modules/@react-native-firebase/storage" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNReanimated: @@ -3210,6 +3228,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e + FirebaseStorage: 436c30aa46f2177ba152f268fe4452118b8a4856 fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -3283,6 +3302,7 @@ SPEC CHECKSUMS: RNFBCrashlytics: f465771d96a2eaf9f6104b30abb002cfe78fc0be RNFBFirestore: e47cdde04ea3d9e73e58e037e1aa1d0b1141c316 RNFBFunctions: 738cc9e2177d060d29b5d143ef2f9ed0eda4bb1f + RNFBStorage: 2dab66f3fcc51de3acd838c72c0ff081e61a0960 RNGestureHandler: 20a4307fd21cbff339abfcfa68192f3f0a6a518b RNReanimated: d51431fd3597a8f8320319dce8e42cee82a5445f RNScreens: 30249f9331c3b00ae7cb7922e11f58b3ed369c07 diff --git a/ios/cally/Info.plist b/ios/cally/Info.plist index 504bab0..6bccf46 100644 --- a/ios/cally/Info.plist +++ b/ios/cally/Info.plist @@ -113,6 +113,7 @@ $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route UILaunchStoryboardName SplashScreen diff --git a/package.json b/package.json index 92e580b..bf18c9a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@react-native-firebase/crashlytics": "^20.3.0", "@react-native-firebase/firestore": "^20.4.0", "@react-native-firebase/functions": "^20.4.0", + "@react-native-firebase/storage": "^21.0.0", "@react-navigation/drawer": "^6.7.2", "@react-navigation/native": "^6.0.2", "date-fns": "^3.6.0", diff --git a/yarn.lock b/yarn.lock index ad2f2f1..a124c7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2425,6 +2425,11 @@ resolved "https://registry.npmjs.org/@react-native-firebase/functions/-/functions-20.4.0.tgz" integrity sha512-g4kAWZboTE9cTdT7KT6k1haHDmEBA36bPCvrh2MJ2RACo2JxotB2MIOEPZ5U/cT94eIAlgI5YtxQQGQfC+VcBQ== +"@react-native-firebase/storage@^21.0.0": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/storage/-/storage-21.0.0.tgz#0905fd67c74629d947f176bfb988d7cc4d85e244" + integrity sha512-meft5Pu0nI7zxhpnP49ko9Uw8GaIy9hXGJfa/fCFrpf2vA9OXdTr3CvgloH/b9DpbkwQGcGTshRqltuttXI67w== + "@react-native/assets-registry@0.74.85": version "0.74.85" resolved "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.85.tgz"