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"