From cbd50e090f2d24023491d58fc7da52bf19d8a652 Mon Sep 17 00:00:00 2001 From: Muhammed Kaplan Date: Thu, 28 Mar 2024 03:15:57 +0100 Subject: [PATCH] wip: change profile picture support --- app/(app)/(tabs)/profile.tsx | 2 +- app/(app)/profile-settings.tsx | 4 +- lib/components/profile_view.tsx | 5 +- lib/components/switch.tsx | 6 + lib/components/toast.tsx | 43 +++-- .../ProfilePictureSection.tsx | 159 ++++++++++++++---- lib/services/storage.service.ts | 36 ++++ lib/tamagui/config.ts | 1 + lib/tamagui/palette.ts | 1 - lib/tamagui/themes.ts | 8 +- package.json | 2 + yarn.lock | 18 ++ 12 files changed, 221 insertions(+), 64 deletions(-) create mode 100644 lib/services/storage.service.ts diff --git a/app/(app)/(tabs)/profile.tsx b/app/(app)/(tabs)/profile.tsx index 0dd8948..ae33226 100644 --- a/app/(app)/(tabs)/profile.tsx +++ b/app/(app)/(tabs)/profile.tsx @@ -8,7 +8,7 @@ import { useNavigation } from 'expo-router'; import { Suspense, useLayoutEffect } from 'react'; import { Spinner } from 'tamagui'; -export default function TabTwoScreen() { +export default function ProfileScreen() { const navigation = useNavigation(); const user = useAuthStore((state) => state.user); diff --git a/app/(app)/profile-settings.tsx b/app/(app)/profile-settings.tsx index 83ff554..45a329d 100644 --- a/app/(app)/profile-settings.tsx +++ b/app/(app)/profile-settings.tsx @@ -2,6 +2,7 @@ import Screen from '@lib/components/screen'; import { QueryKeys } from '@lib/models/query_keys.model'; import { IUser } from '@lib/models/user.model'; import ProfilePictureSection from '@lib/modules/profile-settings/ProfilePictureSection'; +import { profileService } from '@lib/services/profile.service'; import { useAuthStore } from '@lib/store/auth.store'; import { useSuspenseQuery } from '@tanstack/react-query'; import { Suspense } from 'react'; @@ -10,7 +11,8 @@ import { Spinner } from 'tamagui'; export default function ProfileSettingsPage() { const user = useAuthStore((state) => state.user); const profileQuery = useSuspenseQuery({ - queryKey: [QueryKeys.ProfileWithRelations, user?.id] + queryKey: [QueryKeys.ProfileWithRelations, user?.id], + queryFn: () => profileService.fetchProfile(user!.id) }); return ( diff --git a/lib/components/profile_view.tsx b/lib/components/profile_view.tsx index 429a430..e583490 100644 --- a/lib/components/profile_view.tsx +++ b/lib/components/profile_view.tsx @@ -12,7 +12,6 @@ import { Image, ListItem, Separator, - Spinner, Text, View, YGroup, @@ -58,10 +57,10 @@ export default function ProfileView({ > {profileData.profileImageUrl ? ( <> - + - + ) : ( diff --git a/lib/components/switch.tsx b/lib/components/switch.tsx index 71d93b1..8c5d791 100644 --- a/lib/components/switch.tsx +++ b/lib/components/switch.tsx @@ -8,6 +8,12 @@ const AppSwitchFrame = styled(SwitchFrame, { backgroundColor: '$backgroundStrong', borderWidth: 2, borderColor: '$backgroundStrong' + }, + true: { + borderRadius: 1000, + backgroundColor: '$backgroundStrong', + borderWidth: 2, + borderColor: '$backgroundStrong' } }, diff --git a/lib/components/toast.tsx b/lib/components/toast.tsx index 863d113..3fb99c7 100644 --- a/lib/components/toast.tsx +++ b/lib/components/toast.tsx @@ -1,20 +1,20 @@ -import { Toast as IToast, useToastState } from "@tamagui/toast"; -import { useMemo } from "react"; -import { YStack } from "tamagui"; +import { Toast as IToast, useToastState } from '@tamagui/toast'; +import { useMemo } from 'react'; +import { Spinner, XStack, YStack } from 'tamagui'; const Toast = () => { const currentToast = useToastState(); const toastBgColor = useMemo(() => { switch (currentToast?.toastType) { - case "error": - return "red"; - case "success": - return "green"; - case "warning": - return "orange"; + case 'error': + return 'red'; + case 'success': + return 'green'; + case 'warning': + return 'orange'; default: - return "blue"; + return 'lightblue'; } }, [currentToast]); @@ -32,14 +32,21 @@ const Toast = () => { viewportName={currentToast.viewportName} bg={toastBgColor} > - - {currentToast.title} - {!!currentToast.message && ( - - {currentToast.message} - - )} - + {currentToast.isLoading ? ( + + + {currentToast.title} + + ) : ( + + {currentToast.title} + {!!currentToast.message && ( + + {currentToast.message} + + )} + + )} ); }; diff --git a/lib/modules/profile-settings/ProfilePictureSection.tsx b/lib/modules/profile-settings/ProfilePictureSection.tsx index a0538a1..ba0f814 100644 --- a/lib/modules/profile-settings/ProfilePictureSection.tsx +++ b/lib/modules/profile-settings/ProfilePictureSection.tsx @@ -1,28 +1,63 @@ import NO_AVATAR from '@assets/images/no_avatar.svg'; import Button from '@lib/components/button'; +import { QueryKeys } from '@lib/models/query_keys.model'; +import { IUserWithPhoneAndSocial } from '@lib/models/user.model'; import { profileService } from '@lib/services/profile.service'; +import { storageService } from '@lib/services/storage.service'; import { config } from '@tamagui/config/v2-reanimated'; import { Edit3 } from '@tamagui/lucide-icons'; -import { useMutation } from '@tanstack/react-query'; +import { useToastController } from '@tamagui/toast'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as ImagePicker from 'expo-image-picker'; -import React, { useCallback, useState } from 'react'; -import { Avatar, Card, Text, View, XStack, ZStack } from 'tamagui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { ImageSourcePropType } from 'react-native'; +import { Avatar, Card, Spinner, Text, View, XStack, YStack } from 'tamagui'; import { ISectionProps } from '.'; interface IUpdateMutation { - profileImageUrl: string; + profileImageUrl: string | null; } export default function ProfilePictureSection({ user }: ISectionProps) { + const [isLoading, setIsLoading] = useState(false); + const queryClient = useQueryClient(); + const toast = useToastController(); const [selectedImage, setSelectedImage] = useState(); const mutation = useMutation({ mutationFn: ({ profileImageUrl }) => - profileService.updateProfile(user.id, { profileImageUrl }) + profileService.updateProfile(user.id, { profileImageUrl }), + onSuccess: (_data, variables) => { + queryClient.setQueryData( + [QueryKeys.ProfileWithRelations, user.id], + (u) => { + if (!u) return; + return { + ...u, + profileImageUrl: variables.profileImageUrl! + }; + } + ); + } }); - const onSave = useCallback(async () => {}, []); + const onSave = useCallback(async () => { + if (!selectedImage) return; + setIsLoading(true); + + const ref = await storageService.uploadAvatar(user!.id, selectedImage); + + if (ref) { + const photoURL = storageService.getAvatarURL(ref.path); + + await mutation.mutateAsync({ profileImageUrl: photoURL }); + + toast.show('Sparat', { toastType: 'success' }); + } + + setIsLoading(false); + }, [selectedImage]); const onSelect = useCallback(async () => { let result = await ImagePicker.launchImageLibraryAsync({ @@ -38,11 +73,35 @@ export default function ProfilePictureSection({ user }: ISectionProps) { if (result.assets && result.assets.length === 1) { setSelectedImage(result.assets[0]); - console.log(result.assets); + await onSave(); } } }, []); + const onPressRemove = useCallback(async () => { + await mutation.mutateAsync({ + profileImageUrl: null + }); + }, []); + + const profilePicture = useMemo(() => { + if (selectedImage) { + return { + uri: selectedImage.uri + }; + } + + if (user.profileImageUrl) { + return { + uri: user.profileImageUrl + }; + } + + return { + uri: '' + }; + }, [selectedImage, user]); + return ( @@ -50,47 +109,75 @@ export default function ProfilePictureSection({ user }: ISectionProps) { - - - + + - - - - - - + + + + + + + - - {selectedImage && ( - + + {isLoading && ( + + + + Sparar... + )} - - + {user.profileImageUrl && ( + + )} + ); diff --git a/lib/services/storage.service.ts b/lib/services/storage.service.ts new file mode 100644 index 0000000..309d1c3 --- /dev/null +++ b/lib/services/storage.service.ts @@ -0,0 +1,36 @@ +import { decode } from 'base64-arraybuffer'; +import { ImagePickerAsset } from 'expo-image-picker'; +import mime from 'mime'; +import { BaseService } from './base.service'; + +class StorageService extends BaseService { + async uploadAvatar(userUid: string, photo: ImagePickerAsset) { + const mimeType = photo.mimeType; + + if (!mimeType) return; + + const ext = mime.getExtension(photo.mimeType!); + const path = `${userUid}/avatar.${ext}`; + + const { data, error } = await this.client.storage + .from('avatars') + .upload(path, decode(photo.base64!), { + upsert: true, + contentType: mimeType + }); + + if (error) { + throw error; + } + + return data; + } + + getAvatarURL(userUid: string): string { + const { data } = this.client.storage.from('avatars').getPublicUrl(userUid); + + return data.publicUrl; + } +} + +export const storageService = new StorageService(); diff --git a/lib/tamagui/config.ts b/lib/tamagui/config.ts index 24d8584..125dd50 100644 --- a/lib/tamagui/config.ts +++ b/lib/tamagui/config.ts @@ -18,5 +18,6 @@ declare module 'tamagui' { declare module '@tamagui/toast' { interface CustomData { toastType?: 'error' | 'success' | 'warning'; + isLoading?: boolean; } } diff --git a/lib/tamagui/palette.ts b/lib/tamagui/palette.ts index ece6db2..4234157 100644 --- a/lib/tamagui/palette.ts +++ b/lib/tamagui/palette.ts @@ -24,7 +24,6 @@ export const colorPalette_Light = [ export const colorPalette_Dark = [ darkTransparent, - 'hsl(165, 25%, 15%)', 'hsl(165, 64%, 81%)', 'hsl(165, 63%, 76%)', 'hsl(165, 62%, 71%)', diff --git a/lib/tamagui/themes.ts b/lib/tamagui/themes.ts index 557c5b1..a7be1f5 100644 --- a/lib/tamagui/themes.ts +++ b/lib/tamagui/themes.ts @@ -36,13 +36,13 @@ const themesBuilder = createThemeBuilder() navigationBg: -2, navigationCardBg: -2, primary: 7, - background: 1, - backgroundHover: 2, + background: -3, + backgroundHover: -5, backgroundPress: 4, backgroundFocus: 5, backgroundStrong: -2, backgroundTransparent: -0, - color: -7, + color: -9, colorHover: 2, colorPress: 1, colorFocus: -9, @@ -76,7 +76,7 @@ const themesBuilder = createThemeBuilder() mask: 'soften', override: { background: -5, - color: -1 + color: -5 } }, surface: { diff --git a/package.json b/package.json index 1edabb7..ef1e3be 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tanstack/react-query": "^5.27.5", "@tanstack/react-query-persist-client": "^5.27.5", "babel-preset-expo": "^10.0.1", + "base64-arraybuffer": "^1.0.2", "burnt": "^0.12.2", "date-fns": "^3.4.0", "expo": "~50.0.13", @@ -50,6 +51,7 @@ "expo-system-ui": "~2.9.3", "expo-web-browser": "~12.8.2", "immer": "^10.0.4", + "mime": "^4.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "~2.0.4", diff --git a/yarn.lock b/yarn.lock index 8a03d98..dd6d044 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6156,6 +6156,13 @@ __metadata: languageName: node linkType: hard +"base64-arraybuffer@npm:^1.0.2": + version: 1.0.2 + resolution: "base64-arraybuffer@npm:1.0.2" + checksum: 3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f + languageName: node + linkType: hard + "base64-js@npm:^1.2.3, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -10393,6 +10400,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^4.0.1": + version: 4.0.1 + resolution: "mime@npm:4.0.1" + bin: + mime: bin/cli.js + checksum: 8b89fb8d93dca1ce068d072c09faa8e04e85fb1e763197cbf8adaba0aa05eb795197cca332309f724cc2239d99c9c127eccb777d97efddb11aa9e9bcb9538818 + languageName: node + linkType: hard + "mimic-fn@npm:^1.0.0": version: 1.2.0 resolution: "mimic-fn@npm:1.2.0" @@ -13286,6 +13302,7 @@ __metadata: "@tanstack/react-query-persist-client": "npm:^5.27.5" "@types/react": "npm:~18.2.14" babel-preset-expo: "npm:^10.0.1" + base64-arraybuffer: "npm:^1.0.2" burnt: "npm:^0.12.2" date-fns: "npm:^3.4.0" expo: "npm:~50.0.13" @@ -13301,6 +13318,7 @@ __metadata: expo-web-browser: "npm:~12.8.2" husky: "npm:^9.0.11" immer: "npm:^10.0.4" + mime: "npm:^4.0.1" prettier: "npm:^3.2.5" prettier-plugin-organize-imports: "npm:^3.2.4" react: "npm:^18.2.0"