A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

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

feat: add global loading screen

+125 -42
+8 -2
apps/mobile/app/(tabs)/shelf.tsx
··· 268 268 </View> 269 269 <View style={styles.centerContent}> 270 270 <Card style={styles.authCard}> 271 - <CardHeader> 271 + <CardHeader style={styles.authCardHeader}> 272 272 <BookOpen 273 273 size={64} 274 274 color={colors.primary} ··· 340 340 {trackedMovies && trackedMovies.length === 0 && ( 341 341 <View style={styles.centerContent}> 342 342 <Card style={styles.emptyCard}> 343 - <CardHeader> 343 + <CardHeader style={styles.emptyCardHeader}> 344 344 <BookOpen 345 345 size={64} 346 346 color={colors.textSecondary} ··· 504 504 maxWidth: 400, 505 505 alignItems: "center", 506 506 }, 507 + authCardHeader: { 508 + alignItems: "center", 509 + }, 507 510 authIcon: { 508 511 marginBottom: spacing.md, 509 512 }, ··· 522 525 emptyCard: { 523 526 width: "100%", 524 527 maxWidth: 400, 528 + alignItems: "center", 529 + }, 530 + emptyCardHeader: { 525 531 alignItems: "center", 526 532 }, 527 533 emptyIcon: {
+48 -35
apps/mobile/app/_layout.tsx
··· 2 2 import { Stack } from "expo-router"; 3 3 import { StatusBar } from "expo-status-bar"; 4 4 import { useEffect } from "react"; 5 - import { AuthProvider } from "@/contexts/auth"; 5 + import { AuthProvider, useAuth } from "@/contexts/auth"; 6 6 import { ToastProvider } from "@/contexts/toast"; 7 7 import { initializeApiClient } from "@/lib/api"; 8 8 import { queryClient } from "@/lib/query-client"; 9 9 import { colors } from "@/constants/theme"; 10 + import { LoadingScreen } from "@/components/LoadingScreen"; 11 + 12 + function AppContent() { 13 + const { isLoading } = useAuth(); 14 + 15 + if (isLoading) { 16 + return <LoadingScreen message="Loading..." />; 17 + } 18 + 19 + return ( 20 + <ToastProvider> 21 + <Stack 22 + screenOptions={{ 23 + headerStyle: { 24 + backgroundColor: colors.background, 25 + }, 26 + headerTintColor: colors.text, 27 + headerTitleStyle: { 28 + color: colors.text, 29 + }, 30 + contentStyle: { 31 + backgroundColor: colors.background, 32 + }, 33 + }} 34 + > 35 + <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> 36 + <Stack.Screen 37 + name="movie/[id]" 38 + options={{ 39 + title: "Movie Details", 40 + headerTransparent: true, 41 + headerTintColor: colors.text, 42 + }} 43 + /> 44 + <Stack.Screen 45 + name="auth/callback" 46 + options={{ 47 + presentation: "modal", 48 + headerShown: false, 49 + }} 50 + /> 51 + </Stack> 52 + <StatusBar style="light" /> 53 + </ToastProvider> 54 + ); 55 + } 10 56 11 57 export default function RootLayout() { 12 58 useEffect(() => { ··· 16 62 return ( 17 63 <QueryClientProvider client={queryClient}> 18 64 <AuthProvider> 19 - <ToastProvider> 20 - <Stack 21 - screenOptions={{ 22 - headerStyle: { 23 - backgroundColor: colors.background, 24 - }, 25 - headerTintColor: colors.text, 26 - headerTitleStyle: { 27 - color: colors.text, 28 - }, 29 - contentStyle: { 30 - backgroundColor: colors.background, 31 - }, 32 - }} 33 - > 34 - <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> 35 - <Stack.Screen 36 - name="movie/[id]" 37 - options={{ 38 - title: "Movie Details", 39 - headerTransparent: true, 40 - headerTintColor: colors.text, 41 - }} 42 - /> 43 - <Stack.Screen 44 - name="auth/callback" 45 - options={{ 46 - presentation: "modal", 47 - headerShown: false, 48 - }} 49 - /> 50 - </Stack> 51 - <StatusBar style="light" /> 52 - </ToastProvider> 65 + <AppContent /> 53 66 </AuthProvider> 54 67 </QueryClientProvider> 55 68 );
+69
apps/mobile/components/LoadingScreen.tsx
··· 1 + import { Ionicons } from "@expo/vector-icons"; 2 + import { ActivityIndicator, StyleSheet, Text, View } from "react-native"; 3 + import Animated, { 4 + useSharedValue, 5 + useAnimatedStyle, 6 + withRepeat, 7 + withTiming, 8 + Easing, 9 + } from "react-native-reanimated"; 10 + import { useEffect } from "react"; 11 + import { colors, spacing } from "@/constants/theme"; 12 + 13 + interface LoadingScreenProps { 14 + message?: string; 15 + } 16 + 17 + export function LoadingScreen({ message = "Loading..." }: LoadingScreenProps) { 18 + const pulse = useSharedValue(1); 19 + 20 + useEffect(() => { 21 + pulse.value = withRepeat( 22 + withTiming(1.2, { 23 + duration: 1500, 24 + easing: Easing.inOut(Easing.ease), 25 + }), 26 + -1, 27 + true 28 + ); 29 + }, [pulse]); 30 + 31 + const animatedStyle = useAnimatedStyle(() => ({ 32 + transform: [{ scale: pulse.value }], 33 + })); 34 + 35 + return ( 36 + <View style={styles.container}> 37 + <Animated.View style={[styles.logoContainer, animatedStyle]}> 38 + <Ionicons name="film" size={64} color={colors.primary} /> 39 + </Animated.View> 40 + <ActivityIndicator 41 + size="large" 42 + color={colors.primary} 43 + style={styles.spinner} 44 + /> 45 + <Text style={styles.message}>{message}</Text> 46 + </View> 47 + ); 48 + } 49 + 50 + const styles = StyleSheet.create({ 51 + container: { 52 + flex: 1, 53 + backgroundColor: colors.background, 54 + justifyContent: "center", 55 + alignItems: "center", 56 + padding: spacing.lg, 57 + }, 58 + logoContainer: { 59 + marginBottom: spacing.lg, 60 + }, 61 + spinner: { 62 + marginVertical: spacing.md, 63 + }, 64 + message: { 65 + color: colors.textMuted, 66 + fontSize: 16, 67 + fontWeight: "500", 68 + }, 69 + });
-5
backend/src/auth/auth.guard.ts
··· 25 25 26 26 if (authHeader?.startsWith('Bearer ')) { 27 27 sessionId = authHeader.slice(7); 28 - this.logger.debug('Using Bearer token for auth'); 29 28 } else { 30 29 // Cookie stores opaque session id (not DID) 31 30 const cookies = request.cookies as Record<string, string | undefined>; ··· 33 32 } 34 33 35 34 if (!sessionId) { 36 - this.logger.debug('No session cookie or Bearer token found'); 37 35 throw new UnauthorizedException('Not authenticated'); 38 36 } 39 37 40 38 try { 41 39 const sessionRecord = await this.authService.getSessionById(sessionId); 42 40 if (!sessionRecord) { 43 - this.logger.debug('Session not found'); 44 41 throw new UnauthorizedException('Session not found or expired'); 45 42 } 46 43 47 44 // Restore session using OAuth client (refreshes tokens if needed) 48 45 const session = await this.authService.restore(sessionRecord.userDid); 49 46 if (!session) { 50 - this.logger.debug(`No session found for DID: ${sessionRecord.userDid}`); 51 47 throw new UnauthorizedException('Session not found or expired'); 52 48 } 53 49 ··· 61 57 return true; 62 58 } catch (error) { 63 59 if (error instanceof UnauthorizedException) throw error; 64 - this.logger.debug('Failed to restore session', error); 65 60 throw new UnauthorizedException('Invalid or expired session'); 66 61 } 67 62 }