[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.

RECEBA

C3B 72335c31 e02c8631

+201 -33
+196 -18
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, { useState } from 'react'; 9 + import React, { useCallback, useRef, useState, useMemo } from 'react'; 10 10 import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Dimensions, ActivityIndicator } from 'react-native'; 11 + import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; 12 + import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; 13 + import { format } from 'date-fns'; 11 14 12 15 export default function Register() { 13 - 14 - useColorScheme(); 15 16 const colorScheme = useColorScheme(); 16 17 17 18 const styles = StyleSheet.create({ 18 19 container: { 19 - height: Dimensions.get('screen').height, 20 + flex: 1, 20 21 alignItems: 'center', 21 22 justifyContent: 'center', 22 23 backgroundColor: Colors[colorScheme ?? 'light'].background, 23 24 width: '100%', 25 + position: 'relative', 26 + }, 27 + formContainer: { 28 + width: '100%', 29 + alignItems: 'center', 30 + flex: 1, 31 + justifyContent: 'center', 32 + }, 33 + backButton: { 34 + position: 'absolute', 35 + top: 80, 36 + left: 20, 37 + zIndex: 1, 38 + }, 39 + logo: { 40 + marginTop: 80, 24 41 }, 25 42 text: { 26 43 fontSize: 18, ··· 31 48 marginBottom: 10, 32 49 textAlign: 'center', 33 50 }, 51 + datePickerButton: { 52 + flexDirection: 'row', 53 + alignItems: 'center', 54 + borderWidth: 1, 55 + borderColor: Colors[colorScheme ?? 'light'].underlineColor, 56 + borderRadius: 8, 57 + paddingHorizontal: 12, 58 + backgroundColor: Colors[colorScheme ?? 'light'].background, 59 + height: 50, 60 + width: '90%', 61 + marginBottom: 25, 62 + }, 63 + datePickerIcon: { 64 + marginRight: 10, 65 + }, 66 + datePickerText: { 67 + flex: 1, 68 + fontSize: 16, 69 + color: Colors[colorScheme ?? 'light'].text, 70 + }, 71 + datePickerPlaceholder: { 72 + flex: 1, 73 + fontSize: 16, 74 + color: Colors[colorScheme ?? 'light'].textGray, 75 + }, 76 + bottomSheetContent: { 77 + padding: 20, 78 + alignItems: 'center', 79 + backgroundColor: Colors[colorScheme ?? 'light'].background, 80 + paddingBottom: 40, 81 + }, 82 + bottomSheetButton: { 83 + padding: 15, 84 + backgroundColor: Colors[colorScheme ?? 'light'].tint, 85 + borderRadius: 8, 86 + alignItems: 'center', 87 + marginTop: 30, 88 + marginBottom: 30, 89 + width: '80%', 90 + }, 91 + bottomSheetButtonText: { 92 + color: '#fff', 93 + fontWeight: 'bold', 94 + fontSize: 16, 95 + }, 96 + bottomSheetBackground: { 97 + backgroundColor: Colors[colorScheme ?? 'light'].background, 98 + }, 99 + bottomSheetHandle: { 100 + backgroundColor: Colors[colorScheme ?? 'light'].underlineColor, 101 + width: 40, 102 + }, 103 + dateLabelText: { 104 + alignSelf: 'flex-start', 105 + marginLeft: '5%', 106 + marginBottom: 6, 107 + }, 108 + datePickerTitle: { 109 + fontSize: 18, 110 + fontWeight: 'bold', 111 + marginBottom: 20, 112 + }, 113 + chevronIcon: { 114 + marginLeft: 5, 115 + }, 116 + inputStyle: { 117 + width: '90%', 118 + }, 119 + passwordInputStyle: { 120 + width: '90%', 121 + marginBottom: 25, 122 + }, 34 123 }); 35 124 125 + const bottomSheetRef = useRef<BottomSheet>(null); 126 + 127 + // Use fixed height for bottom sheet 128 + const snapPoints = useMemo(() => ['60%'], []); 129 + 130 + const handleSheetChanges = useCallback((index: number) => { 131 + console.log('handleSheetChanges', index); 132 + }, []); 133 + 134 + const openBottomSheet = useCallback(() => { 135 + bottomSheetRef.current?.expand(); 136 + }, []); 137 + 138 + const closeBottomSheet = useCallback(() => { 139 + bottomSheetRef.current?.close(); 140 + }, []); 141 + 36 142 const [email, setEmail] = useState(''); 37 143 const [handle, setHandle] = useState(''); 38 144 const [password, setPassword] = useState(''); 39 145 const [inviteCode, setInviteCode] = useState(''); 146 + const [birthDate, setBirthDate] = useState<Date>(new Date()); 40 147 const [loading, setLoading] = useState(false); 41 148 const [error, setError] = useState(''); 42 149 150 + const handleDateChange = (event: DateTimePickerEvent, selectedDate: Date | undefined) => { 151 + if (selectedDate) { 152 + setBirthDate(selectedDate); 153 + } 154 + }; 155 + 156 + const confirmDateSelection = () => { 157 + closeBottomSheet(); 158 + }; 159 + 43 160 const handleRegister = async () => { 44 - if (!email || !handle || !password) { 161 + if (!email || !handle || !password || !birthDate) { 45 162 setError('Please fill in all required fields'); 46 163 return; 47 164 } ··· 55 172 56 173 // Simple handle validation 57 174 if (!handle.includes('.')) { 58 - setError('Handle must be in format username.bsky.social'); 175 + setError('Handle must be in format username.sprk.so'); 59 176 return; 60 177 } 61 178 ··· 86 203 <SafeAreaView style={styles.container}> 87 204 <TouchableOpacity 88 205 onPress={() => router.back()} 89 - style={{ position: 'absolute', top: 80, left: 20, zIndex: 1 }}> 206 + style={styles.backButton}> 90 207 <Ionicons name="chevron-back" size={24} color={Colors[colorScheme ?? 'light'].text} /> 91 208 </TouchableOpacity> 92 - <Logo size={14} color={Colors[colorScheme ?? 'light'].selectedIcon} style={{ marginBottom: 50 }} /> 93 - <View style={{ width: '100%', alignItems: 'center' }}> 209 + 210 + <Logo size={14} color={Colors[colorScheme ?? 'light'].selectedIcon} style={styles.logo} /> 211 + 212 + <View style={styles.formContainer}> 94 213 {error ? <ThemedText style={styles.errorText}>{error}</ThemedText> : null} 95 - 214 + 96 215 <InputArea 97 216 label='Email' 98 217 placeholder='Enter your email' ··· 101 220 inputStyle={{ width: '100%' }} 102 221 value={email} 103 222 onChangeText={setEmail} 104 - style={{ width: "90%" }} 223 + style={styles.inputStyle} 105 224 /> 106 - 225 + 107 226 <InputArea 108 227 label='Handle' 109 228 placeholder='username.sprk.so' ··· 112 231 inputStyle={{ width: '100%' }} 113 232 value={handle} 114 233 onChangeText={setHandle} 115 - style={{ width: "90%" }} 234 + style={styles.inputStyle} 116 235 /> 117 - 236 + 118 237 <InputArea 119 238 label='Password' 120 239 placeholder='Enter your password' ··· 123 242 inputStyle={{ width: '90%' }} 124 243 value={password} 125 244 onChangeText={setPassword} 126 - style={{ width: "90%", marginBottom: 15 }} 245 + style={styles.passwordInputStyle} 127 246 /> 128 - 247 + 129 248 <InputArea 130 249 label='Invite Code (Optional)' 131 250 placeholder='Enter invite code if you have one' ··· 134 253 inputStyle={{ width: '90%' }} 135 254 value={inviteCode} 136 255 onChangeText={setInviteCode} 137 - style={{ width: "90%", marginBottom: 30 }} 256 + style={styles.inputStyle} 138 257 /> 139 - 258 + 259 + {/* Birth Date Picker Button */} 260 + <ThemedText type='description' style={styles.dateLabelText}>Birth Date</ThemedText> 261 + <TouchableOpacity 262 + style={styles.datePickerButton} 263 + onPress={openBottomSheet} 264 + > 265 + <Ionicons 266 + name="calendar-outline" 267 + size={20} 268 + color={Colors[colorScheme ?? 'light'].icon} 269 + style={styles.datePickerIcon} 270 + /> 271 + {birthDate ? ( 272 + <ThemedText style={styles.datePickerText}> 273 + {format(birthDate, 'MMMM dd, yyyy')} 274 + </ThemedText> 275 + ) : ( 276 + <ThemedText style={styles.datePickerPlaceholder}> 277 + Select your birth date 278 + </ThemedText> 279 + )} 280 + <Ionicons 281 + name="chevron-down" 282 + size={20} 283 + color={Colors[colorScheme ?? 'light'].icon} 284 + style={styles.chevronIcon} 285 + /> 286 + </TouchableOpacity> 287 + 140 288 <ActionButton 141 289 title={loading ? "Registering..." : "Register"} 142 290 onPress={handleRegister} ··· 153 301 <ThemedText>Already have an account? Login</ThemedText> 154 302 </TouchableOpacity> 155 303 </View> 304 + 305 + {/* Bottom Sheet for Date Picker - outside of the form container */} 306 + <BottomSheet 307 + ref={bottomSheetRef} 308 + index={-1} 309 + snapPoints={snapPoints} 310 + onChange={handleSheetChanges} 311 + enablePanDownToClose 312 + backgroundStyle={styles.bottomSheetBackground} 313 + handleIndicatorStyle={styles.bottomSheetHandle} 314 + > 315 + <BottomSheetView style={styles.bottomSheetContent}> 316 + <ThemedText style={styles.datePickerTitle}> 317 + Select Your Birth Date 318 + </ThemedText> 319 + <DateTimePicker 320 + value={birthDate} 321 + mode="date" 322 + display="spinner" 323 + onChange={handleDateChange} 324 + maximumDate={new Date()} 325 + /> 326 + <TouchableOpacity 327 + style={styles.bottomSheetButton} 328 + onPress={confirmDateSelection} 329 + > 330 + <ThemedText style={styles.bottomSheetButtonText}>Done</ThemedText> 331 + </TouchableOpacity> 332 + </BottomSheetView> 333 + </BottomSheet> 156 334 </SafeAreaView> 157 335 ); 158 336 }
+5 -15
app/(tabs)/ProfileScreen.tsx
··· 68 68 const { isLoggedIn, session, agent, logout } = useAtProto(); 69 69 70 70 // This is the user's own profile if logged in 71 - const isMine = true; 71 + const isMine = !true; 72 72 73 73 // User's DID comes from session when logged in, otherwise use mock DID 74 74 const userDid = isLoggedIn && session ? session.did : did; ··· 88 88 }; 89 89 90 90 useEffect(() => { 91 - // When user is logged in OR (user is not logged in but viewing someone else's profile) 92 91 if (isLoggedIn || (!isLoggedIn && !isMine)) { 93 92 const loadProfileData = async () => { 94 93 try { ··· 143 142 loadProfileData(); 144 143 loadVideoPosts(); 145 144 } else if (!isLoggedIn && isMine) { 146 - // When user is not logged in and viewing their own profile 147 145 setUserData({ 148 146 id: '', 149 147 did: '', ··· 227 225 justifyContent: 'center', 228 226 height: '70%', 229 227 }, 230 - profileContent: { 228 + profileContent: { 231 229 marginTop: 20, 230 + height: '100%', 232 231 }, 233 232 profileTabs: { 234 233 flexDirection: 'row', ··· 286 285 contentContainerStyle={styles.scrollViewContent} 287 286 showsVerticalScrollIndicator={false} 288 287 > 289 - {/* Navbar Superior */} 290 288 <View style={styles.profileNavbar}> 291 289 <TouchableOpacity onPress={() => {}}> 292 290 <Ionicons ··· 317 315 </TouchableOpacity> 318 316 </View> 319 317 320 - {/* Cabeçalho e foto do perfil */} 321 - <View style={styles.profileHeader}> 318 + <View style={!isLoggedIn && isMine ? styles.profileHeaderNull : styles.profileHeader}> 322 319 {userData && <ProfilePicture userData={userData} />} 323 320 {userData && <ProfileInfo userData={userData} />} 324 321 ··· 332 329 333 330 { 334 331 !isLoggedIn && isMine && ( 335 - // Not logged in and viewing own profile => show Login / Register buttons 336 332 <View style={styles.profileActionButtonsVertical}> 337 333 <ActionButton 338 334 type="primary" ··· 351 347 } 352 348 { 353 349 !isLoggedIn && !isMine && ( 354 - // Not logged in and viewing someone else's profile => show "Follow" (redirects to login) 355 350 <ActionButton 356 351 type="primary" 357 352 title="Follow" ··· 362 357 } 363 358 { 364 359 isLoggedIn && !isMine && ( 365 - // Logged in and viewing someone else's profile => show "Follow" with function 366 360 <ActionButton 367 361 type="primary" 368 362 title="Follow" 369 363 onPress={() => { 370 - // Follow logic here 371 364 console.log("followed " + userData?.did); 372 365 }} 373 366 width={250} ··· 377 370 { 378 371 isLoggedIn && isMine && ( 379 372 // Logged in and viewing own profile => Show logout button 380 - <View style={styles.profileActionButtonsVertical}> 373 + <View style={styles.profileActionButtons}> 381 374 <ActionButton 382 375 type="outline" 383 376 title="Logout" ··· 389 382 } 390 383 </View> 391 384 392 - {/* Tabs (Ex: Videos e Fotos) */} 393 385 <View style={styles.profileContent}> 394 - {/* Show tabs for everyone except when not logged in and viewing own profile */} 395 386 { (isLoggedIn || (!isLoggedIn && !isMine)) && 396 387 <View style={styles.profileTabs}> 397 388 <View style={styles.tabButton}> ··· 424 415 </View> 425 416 </View> 426 417 } 427 - {/* Grid de vídeos - Only show when logged in or viewing someone else's profile */} 428 418 { (isLoggedIn || (!isLoggedIn && !isMine)) && 429 419 <View style={styles.videoGrid}> 430 420 {paddedVideoData.map((item, index) => {