[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Arrumado os trem de android

arrumei os trem do index, tem q arrumar agora o profile feed q ta fudido.

C3B 30cd089b 577670c0

+286 -56
+90 -22
app/(auth)/Register.tsx
··· 6 6 import { pdsRegister } from '@/api/pdsAuth'; 7 7 import { Ionicons } from '@expo/vector-icons'; 8 8 import { router } from 'expo-router'; 9 - import React, { useCallback, useRef, useState, useMemo } from 'react'; 10 - import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Dimensions, ActivityIndicator } from 'react-native'; 9 + import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'; 10 + import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Dimensions, ActivityIndicator, Animated } from 'react-native'; 11 11 import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; 12 12 import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; 13 13 import { format } from 'date-fns'; 14 + import Reanimated, { withTiming, useAnimatedStyle, useSharedValue, withSequence, SlideInUp, SlideOutDown } from 'react-native-reanimated'; 15 + 16 + // Define the domain constant 17 + const DOMAIN = '.sprk.so'; 14 18 15 19 export default function Register() { 16 20 const colorScheme = useColorScheme(); ··· 120 124 width: '90%', 121 125 marginBottom: 25, 122 126 }, 127 + handleInputContainer: { 128 + flexDirection: 'row', 129 + alignItems: 'center', 130 + width: '90%', 131 + marginBottom: 25, 132 + }, 133 + handleDomain: { 134 + fontSize: 16, 135 + color: Colors[colorScheme ?? 'light'].textGray, 136 + paddingRight: 12, 137 + marginLeft: 5, 138 + }, 139 + toastContainer: { 140 + position: 'absolute', 141 + top: 100, 142 + left: 20, 143 + right: 20, 144 + backgroundColor: 'rgba(255, 0, 0, 0.8)', 145 + padding: 15, 146 + borderRadius: 8, 147 + alignItems: 'center', 148 + zIndex: 1000, 149 + }, 150 + toastText: { 151 + color: 'white', 152 + fontWeight: 'bold', 153 + }, 123 154 }); 124 155 125 156 const bottomSheetRef = useRef<BottomSheet>(null); ··· 140 171 }, []); 141 172 142 173 const [email, setEmail] = useState(''); 143 - const [handle, setHandle] = useState(''); 174 + const [username, setUsername] = useState(''); 144 175 const [password, setPassword] = useState(''); 145 176 const [inviteCode, setInviteCode] = useState(''); 146 177 const [birthDate, setBirthDate] = useState<Date>(new Date()); 147 178 const [loading, setLoading] = useState(false); 148 179 const [error, setError] = useState(''); 180 + const [showToast, setShowToast] = useState(false); 181 + 182 + // Error toast timing effect 183 + useEffect(() => { 184 + if (error) { 185 + setShowToast(true); 186 + const timer = setTimeout(() => { 187 + setShowToast(false); 188 + }, 3000); 189 + return () => clearTimeout(timer); 190 + } 191 + }, [error]); 149 192 150 193 const handleDateChange = (event: DateTimePickerEvent, selectedDate: Date | undefined) => { 151 194 if (selectedDate) { ··· 157 200 closeBottomSheet(); 158 201 }; 159 202 203 + // Function to get the full handle with domain 204 + const getFullHandle = () => { 205 + return `${username}${DOMAIN}`; 206 + }; 207 + 160 208 const handleRegister = async () => { 161 - if (!email || !handle || !password || !birthDate) { 209 + const trimmedEmail = email.trim(); 210 + const trimmedUsername = username.trim(); 211 + 212 + if (!trimmedEmail || !trimmedUsername || !password || !birthDate) { 162 213 setError('Please fill in all required fields'); 163 214 return; 164 215 } 165 216 166 - // Simple email validation 217 + // Improved email validation with proper trimming 167 218 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 168 - if (!emailRegex.test(email)) { 219 + if (!emailRegex.test(trimmedEmail)) { 169 220 setError('Please enter a valid email address'); 170 221 return; 171 222 } 172 223 173 - // Simple handle validation 174 - if (!handle.includes('.')) { 175 - setError('Handle must be in format username.sprk.so'); 224 + // Username validation (no dots, no spaces, alphanumeric only) 225 + const usernameRegex = /^[a-zA-Z0-9_-]+$/; 226 + if (!usernameRegex.test(trimmedUsername)) { 227 + setError('Username can only contain letters, numbers, underscores, and hyphens'); 176 228 return; 177 229 } 178 230 ··· 186 238 setLoading(true); 187 239 setError(''); 188 240 241 + // Get the full handle with domain and use it for registration 242 + const fullHandle = getFullHandle(); 243 + 189 244 // Call the pdsRegister function from our API 190 - await pdsRegister(email, handle, password, inviteCode || undefined); 245 + await pdsRegister(trimmedEmail, fullHandle, password, inviteCode || undefined); 191 246 192 247 // If successful, navigate to the app's main screen 193 248 router.replace('/(tabs)'); ··· 209 264 210 265 <Logo size={14} color={Colors[colorScheme ?? 'light'].selectedIcon} style={styles.logo} /> 211 266 212 - <View style={styles.formContainer}> 213 - {error ? <ThemedText style={styles.errorText}>{error}</ThemedText> : null} 267 + {/* Animated Toast for Errors */} 268 + {showToast && ( 269 + <Reanimated.View 270 + style={styles.toastContainer} 271 + entering={SlideInUp} 272 + exiting={SlideOutDown} 273 + > 274 + <ThemedText style={styles.toastText}>{error}</ThemedText> 275 + </Reanimated.View> 276 + )} 214 277 278 + <View style={styles.formContainer}> 215 279 <InputArea 216 280 label='Email' 217 281 placeholder='Enter your email' ··· 223 287 style={styles.inputStyle} 224 288 /> 225 289 226 - <InputArea 227 - label='Handle' 228 - placeholder='username.sprk.so' 229 - icon='at' 230 - type='text' 231 - inputStyle={{ width: '100%' }} 232 - value={handle} 233 - onChangeText={setHandle} 234 - style={styles.inputStyle} 235 - /> 290 + {/* Custom username input with domain appended */} 291 + <View style={styles.handleInputContainer}> 292 + <InputArea 293 + label='Username' 294 + placeholder='Choose your username' 295 + icon='at' 296 + type='text' 297 + inputStyle={{ flex: 1, paddingRight: 0 }} 298 + value={username} 299 + onChangeText={setUsername} 300 + style={{ flex: 1 }} 301 + /> 302 + <ThemedText style={styles.handleDomain}>{DOMAIN}</ThemedText> 303 + </View> 236 304 237 305 <InputArea 238 306 label='Password'
+131 -17
app/(tabs)/ProfileScreen.tsx
··· 1 - import React, { useEffect, useState } from 'react'; 1 + import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; 2 2 import { 3 + Platform, 3 4 SafeAreaView, 4 5 ScrollView, 5 6 StyleSheet, 6 7 TouchableOpacity, 7 8 useColorScheme, 8 9 View, 10 + Image, 11 + FlatList, 12 + RefreshControl, 13 + Alert, 14 + ActivityIndicator 9 15 } from 'react-native'; 10 16 import { Ionicons } from '@expo/vector-icons'; 11 17 import { ThemedText } from '@/components/ThemedText'; ··· 18 24 import PlaceholderVideoDisplay from '@/components/Profile/PlaceholderVideoDisplay'; 19 25 import { did } from '@/constants/MockData'; 20 26 import { UserProps, PostProps } from '@/types/Interfaces'; 21 - import { router, useRouter } from 'expo-router'; 27 + import { router, useRouter, useLocalSearchParams } from 'expo-router'; 22 28 import { getProfile, getProfileMedia } from '@/api/profileServices'; 23 29 import useAtProto from '@/hooks/useAtProto'; 30 + import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; 24 31 25 32 function padVideosWithPlaceholders( 26 33 videos: (PostProps & { isPlaceholder?: boolean })[] ··· 63 70 export default function ProfileScreen() { 64 71 const colorScheme = useColorScheme(); 65 72 const route = useRouter(); 66 - 73 + const params = useLocalSearchParams(); 74 + 67 75 const { isLoggedIn, session, agent, logout } = useAtProto(); 68 - 69 - const isMine = !true; 70 - 71 - const userDid = isLoggedIn && session ? session.did : did; 76 + 77 + const profileDid = typeof params.did === 'string' && params.did ? 78 + params.did : 79 + (isLoggedIn && session ? session.did : did); 80 + 81 + const isMine = isLoggedIn && session && profileDid === session.did; 72 82 73 83 const [userData, setUserData] = useState<UserProps | null>(null); 74 84 const [videoPosts, setVideoPosts] = useState<PostProps[]>([]); 85 + 86 + const bottomSheetRef = useRef<BottomSheet>(null); 87 + const snapPoints = useMemo(() => ['25%'], []); 88 + 89 + const handleSheetChanges = useCallback((index: number) => { 90 + console.log('handleSheetChanges', index); 91 + }, []); 92 + 93 + const openBottomSheet = useCallback(() => { 94 + bottomSheetRef.current?.expand(); 95 + }, []); 96 + 97 + const closeBottomSheet = useCallback(() => { 98 + bottomSheetRef.current?.close(); 99 + }, []); 75 100 76 101 const loadVideoPosts = async () => { 77 102 try { 78 - const mediaPosts = await getProfileMedia(userDid, 'video'); 103 + const mediaPosts = await getProfileMedia(profileDid, 'video'); 79 104 const posts = mediaPosts.map((item: any) => item.post); 80 105 setVideoPosts(posts); 81 106 } catch (error) { ··· 87 112 if (isLoggedIn || (!isLoggedIn && !isMine)) { 88 113 const loadProfileData = async () => { 89 114 try { 90 - const profileData = await getProfile(userDid); 115 + const profileData = await getProfile(profileDid); 91 116 if (profileData) { 92 117 setUserData({ 93 118 id: profileData.did, 94 119 did: profileData.did, 95 - displayName: profileData.displayName || (session?.handle || ''), 96 - handle: profileData.handle || (session?.handle || ''), 120 + displayName: profileData.displayName || (isMine && session?.handle ? session.handle : ''), 121 + handle: profileData.handle || (isMine && session?.handle ? session.handle : ''), 97 122 description: profileData.description || '', 98 123 avatar: profileData.avatar || '', 99 124 banner: profileData.banner || '', ··· 112 137 } catch (error) { 113 138 console.error('Error loading profile:', error); 114 139 115 - if (isLoggedIn && session) { 140 + if (isLoggedIn && session && isMine) { 116 141 setUserData({ 117 142 id: session.did, 118 143 did: session.did, ··· 151 176 labels: [], 152 177 }); 153 178 } 154 - }, [isLoggedIn, isMine, session, userDid]); 179 + }, [isLoggedIn, isMine, session, profileDid]); 155 180 156 181 const paddedVideoData = padVideosWithPlaceholders(videoPosts); 157 182 ··· 162 187 params: { 163 188 videoData: JSON.stringify(videoPosts), 164 189 initialIndex: index.toString(), 190 + animation: Platform.OS === 'android' ? 'fade' : 'none' 165 191 }, 166 192 }); 167 193 } ··· 175 201 } 176 202 } 177 203 204 + const handleProfileSettings = () => { 205 + closeBottomSheet(); 206 + console.log('Navigate to profile settings'); 207 + }; 208 + 178 209 const handleLogout = async () => { 179 210 try { 211 + closeBottomSheet(); 180 212 await logout(); 181 213 console.log('User logged out'); 182 214 } catch (error) { ··· 252 284 gap: 10, 253 285 width: '100%', 254 286 }, 287 + bottomSheetContent: { 288 + padding: 20, 289 + backgroundColor: Colors[colorScheme ?? 'light'].background, 290 + }, 291 + bottomSheetOption: { 292 + flexDirection: 'row', 293 + alignItems: 'center', 294 + paddingVertical: 15, 295 + borderBottomWidth: 1, 296 + borderBottomColor: Colors[colorScheme ?? 'light'].underlineColor, 297 + }, 298 + bottomSheetOptionText: { 299 + marginLeft: 15, 300 + fontSize: 16, 301 + color: Colors[colorScheme ?? 'light'].text, 302 + }, 303 + bottomSheetOptionTextDanger: { 304 + marginLeft: 15, 305 + fontSize: 16, 306 + color: '#FF3B30', 307 + }, 308 + bottomSheetBackground: { 309 + backgroundColor: Colors[colorScheme ?? 'light'].background, 310 + }, 311 + bottomSheetHandle: { 312 + backgroundColor: Colors[colorScheme ?? 'light'].underlineColor, 313 + }, 255 314 }); 256 315 257 316 return ( ··· 262 321 showsVerticalScrollIndicator={false} 263 322 > 264 323 <View style={styles.profileNavbar}> 265 - <TouchableOpacity onPress={() => {}}> 324 + <TouchableOpacity onPress={() => { 325 + if (params.did) { 326 + router.back(); 327 + } 328 + }}> 266 329 <Ionicons 267 330 name="chevron-back" 268 331 size={24} 269 332 color={Colors[colorScheme ?? 'light'].text} 333 + style={{ opacity: params.did ? 1 : 0 }} 270 334 /> 271 335 </TouchableOpacity> 272 336 273 337 <ThemedText style={styles.profileTopText}> 274 - {isLoggedIn ? 'My Profile' : userData?.displayName ?? ''} 338 + {isMine ? 'My Profile' : userData?.displayName ?? ''} 275 339 </ThemedText> 276 340 277 - <TouchableOpacity onPress={() => {}}> 278 - {isLoggedIn ? ( 341 + <TouchableOpacity onPress={openBottomSheet}> 342 + {isLoggedIn && isMine ? ( 279 343 <Ionicons 280 344 name="settings-outline" 281 345 size={24} ··· 403 467 </View> 404 468 </ScrollView> 405 469 </ContentWrapper> 470 + 471 + <BottomSheet 472 + ref={bottomSheetRef} 473 + index={-1} 474 + snapPoints={snapPoints} 475 + onChange={handleSheetChanges} 476 + enablePanDownToClose 477 + backgroundStyle={styles.bottomSheetBackground} 478 + handleIndicatorStyle={styles.bottomSheetHandle} 479 + > 480 + <BottomSheetView style={styles.bottomSheetContent}> 481 + <TouchableOpacity 482 + style={styles.bottomSheetOption} 483 + onPress={handleProfileSettings} 484 + > 485 + <Ionicons 486 + name="person-circle-outline" 487 + size={24} 488 + color={Colors[colorScheme ?? 'light'].text} 489 + /> 490 + <ThemedText style={styles.bottomSheetOptionText}> 491 + Profile Settings 492 + </ThemedText> 493 + </TouchableOpacity> 494 + 495 + <TouchableOpacity 496 + style={styles.bottomSheetOption} 497 + onPress={handleLogout} 498 + > 499 + <Ionicons 500 + name="log-out-outline" 501 + size={24} 502 + color="#FF3B30" 503 + /> 504 + <ThemedText style={styles.bottomSheetOptionTextDanger}> 505 + Logout 506 + </ThemedText> 507 + </TouchableOpacity> 508 + </BottomSheetView> 509 + </BottomSheet> 406 510 </SafeAreaView> 407 511 ); 512 + } 513 + 514 + // Utility function to navigate to a profile 515 + export function navigateToProfile(did: string) { 516 + // If navigating to a profile, always include the DID parameter 517 + // This allows the correct handling when coming from different contexts 518 + router.push({ 519 + pathname: "/(tabs)/ProfileScreen", 520 + params: { did } 521 + }); 408 522 }
+23 -4
app/(tabs)/_layout.tsx
··· 10 10 import { UserProps } from '@/types/Interfaces'; 11 11 import { getProfile } from '@/api/profileServices'; 12 12 import { did } from '@/constants/MockData'; 13 + import useAtProto from '@/hooks/useAtProto'; 14 + import { router } from 'expo-router'; 13 15 14 16 export default function TabLayout() { 15 17 const colorScheme = useColorScheme(); 16 18 17 19 const [userData, setUserData] = useState<UserProps | null>(null); 18 - const isLoggedIn = false; 20 + const { isLoggedIn, session } = useAtProto(); 19 21 20 22 21 23 useEffect(() => { 22 24 if (isLoggedIn) { 23 25 const loadProfileData = async () => { 24 26 try { 25 - const profileData = await getProfile(did); 27 + const profileData = await getProfile(session.did); 26 28 if (profileData) { 27 29 setUserData({ 28 30 id: profileData.did, ··· 90 92 flexDirection: 'row', 91 93 borderWidth: 0, 92 94 }, 95 + android: { 96 + backgroundColor: Colors[colorScheme ?? 'light'].background, 97 + alignItems: 'center', 98 + justifyContent: 'space-between', 99 + display: 'flex', 100 + flexDirection: 'row', 101 + borderWidth: 0, 102 + borderTopWidth: 0, 103 + elevation: 8, 104 + }, 93 105 default: { 94 106 backgroundColor: Colors[colorScheme ?? 'light'].background, 95 107 alignItems: 'center', ··· 99 111 borderWidth: 0, 100 112 }, 101 113 }), 102 - }}> 114 + }} 115 + safeAreaInsets={{ bottom: 0 }} 116 + > 103 117 <Tabs.Screen 104 118 name="index" 105 119 options={{ ··· 165 179 alignItems: 'center', 166 180 flexDirection: 'row', 167 181 }, 168 - tabBarIcon: ({ focused }) => userData ? <ProfileIcon userData={userData} isSelected={focused} size={28} /> : null, 182 + tabBarIcon: ({ focused }) => userData ? <ProfileIcon uri={userData.avatar} isSelected={focused} size={28} /> : null, 169 183 }} 184 + listeners={({ navigation }) => ({ 185 + tabPress: () => { 186 + router.replace('/(tabs)/ProfileScreen'); 187 + }, 188 + })} 170 189 /> 171 190 </Tabs> 172 191 );
+20 -6
app/(tabs)/index.tsx
··· 4 4 FlatList, 5 5 View, 6 6 Dimensions, 7 + Platform, 8 + SafeAreaView, 7 9 } from 'react-native'; 8 10 import { ThemedView } from '@/components/ThemedView'; 9 11 import VideoScreen from '@/components/Video/VideoScreen'; ··· 31 33 viewAreaCoveragePercentThreshold: 95, 32 34 }); 33 35 34 - const { height: windowHeight } = Dimensions.get('screen'); 36 + const { height: windowHeight } = Dimensions.get('window'); 35 37 const TAB_BAR_HEIGHT = useBottomTabBarHeight(); 38 + // Calculate available height properly for both platforms 36 39 const availableHeight = windowHeight - TAB_BAR_HEIGHT; 37 40 38 41 const shuffleArray = (array: any[]) => { 39 42 return array.sort(() => Math.random() - 0.5); 40 43 }; 41 - 42 44 43 45 useEffect(() => { 44 46 const loadContent = async () => { ··· 54 56 55 57 loadContent(); 56 58 }, []); 57 - 58 59 59 60 return ( 60 - <ThemedView > 61 + <ThemedView style={styles.container}> 61 62 <VideoTop /> 62 63 <FlatList 63 64 ref={flatListRef} ··· 68 69 const embedType = item.embed?.$type || ''; 69 70 70 71 return ( 71 - <View style={ { height: availableHeight }}> 72 + <View style={[styles.itemContainer, { height: availableHeight }]}> 72 73 {embedType === 'app.bsky.embed.video' || 73 74 embedType === 'app.bsky.embed.video#view' ? ( 74 75 <VideoScreen videoData={{ ...item, isActive }} /> ··· 88 89 maxToRenderPerBatch={2} 89 90 removeClippedSubviews 90 91 scrollEventThrottle={16} 91 - style={{ height: availableHeight, backgroundColor: 'black' }} 92 + contentContainerStyle={Platform.OS === 'android' ? { paddingBottom: TAB_BAR_HEIGHT } : undefined} 93 + style={styles.flatList} 92 94 /> 93 95 </ThemedView> 94 96 ); 95 97 } 98 + 99 + const styles = StyleSheet.create({ 100 + container: { 101 + flex: 1, 102 + }, 103 + flatList: { 104 + backgroundColor: 'black', 105 + }, 106 + itemContainer: { 107 + width: '100%', 108 + } 109 + });
+9 -2
components/Search/FeaturedProfile.tsx
··· 4 4 import { ThemedText } from "../ThemedText"; 5 5 import { Ionicons } from "@expo/vector-icons"; 6 6 import { Colors } from "@/constants/Colors"; 7 + import { navigateToProfile } from "@/app/(tabs)/ProfileScreen"; 7 8 8 9 const formatNumber = (num?: number) => { 9 10 if (!num) return '0'; ··· 73 74 }); 74 75 75 76 return ( 76 - <TouchableOpacity style={styles.container}> 77 + <TouchableOpacity style={styles.container} onPress={() => navigateToProfile(user.did)}> 77 78 <Image source={{ uri: user.avatar }} style={styles.profilePicture} /> 78 79 <View style={styles.infoContainer}> 79 80 <View style={styles.nameContainer}> ··· 96 97 <ThemedText style={styles.stat}>{formatNumber(user.followsCount)} Following</ThemedText> 97 98 </View> 98 99 </View> 99 - <TouchableOpacity style={styles.followButton} onPress={onFollow}> 100 + <TouchableOpacity 101 + style={styles.followButton} 102 + onPress={(e) => { 103 + e.stopPropagation(); // Prevent triggering parent's onPress 104 + onFollow(); 105 + }} 106 + > 100 107 <ThemedText style={styles.followButtonText}>{isFollowing ? <Ionicons name="person-remove" size={16} color="#fff" /> : <Ionicons name="person-add" size={16} color="#fff" />}</ThemedText> 101 108 </TouchableOpacity> 102 109 </TouchableOpacity>
+1 -1
components/Video/VideoScreen.tsx
··· 122 122 nativeControls={false} 123 123 allowsVideoFrameAnalysis={false} 124 124 /> 125 - <BlurView intensity={50} style={styles.blurOverlay} tint="dark" /> 125 + <BlurView intensity={50} style={styles.blurOverlay} tint="dark" experimentalBlurMethod="dimezisBlurView" /> 126 126 127 127 <Image 128 128 style={styles.videoBackground}
+10 -2
components/Video/VideoSide.tsx
··· 4 4 import { ThemedText } from '../ThemedText'; 5 5 import { Colors } from '@/constants/Colors'; 6 6 import { VideoSideProps } from '@/types/Interfaces'; 7 + import { navigateToProfile } from '@/app/(tabs)/ProfileScreen'; 7 8 8 9 const VideoSide: React.FC<VideoSideProps> = ({ videoData, onComments }) => { 9 10 const colorScheme = useColorScheme(); ··· 68 69 <View style={styles.container} pointerEvents="box-none"> 69 70 <View style={styles.buttonsList}> 70 71 <TouchableOpacity style={styles.iconContainer}> 71 - <View style={styles.profilePicWrapper}> 72 + <TouchableOpacity 73 + style={styles.profilePicWrapper} 74 + onPress={() => { 75 + if (videoData.author?.did) { 76 + navigateToProfile(videoData.author.did); 77 + } 78 + }} 79 + > 72 80 <Image 73 81 source={{ uri: videoData.author?.avatar || '' }} 74 82 style={styles.profilePic} 75 83 /> 76 - </View> 84 + </TouchableOpacity> 77 85 <TouchableOpacity onPress={() => console.log('followed')} style={styles.addIcon}> 78 86 <Ionicons color={Colors[colorScheme ?? 'light'].tint} name="add-circle-sharp" size={30} style={{ position: 'absolute', bottom: 0, left: 0 }} /> 79 87 <Ionicons color="#FFFFFF" name="add-circle-outline" size={30} style={{ position: 'absolute', bottom: 0, left: 0 }} />
+2 -2
components/global/ProfileIcon.tsx
··· 4 4 import { View, Image, StyleSheet, useColorScheme } from 'react-native'; 5 5 6 6 7 - const ProfileIcon: React.FC<{ userData: UserProps, isSelected: boolean, size: number }> = ({ userData, isSelected, size }) => { 7 + const ProfileIcon: React.FC<{ uri: string, isSelected: boolean, size: number }> = ({ uri, isSelected, size }) => { 8 8 const colorScheme = useColorScheme(); 9 9 10 10 const styles = StyleSheet.create({ ··· 32 32 { width: size, height: size, borderRadius: size / 2 }, 33 33 isSelected && styles.selectedBorder 34 34 ]}> 35 - <Image source={{ uri: userData.avatar }} style={styles.image} /> 35 + <Image source={{ uri }} style={styles.image} /> 36 36 </View> 37 37 ); 38 38 };