mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
nostr login and initial settings component, workout template details component
This commit is contained in:
parent
bd123017f4
commit
05d3c02523
3
app.json
3
app.json
@ -54,7 +54,8 @@
|
|||||||
"enableFTS": true
|
"enableFTS": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
@ -207,7 +207,7 @@ export default function TemplatesScreen() {
|
|||||||
<View className="h-20" />
|
<View className="h-20" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Rest of the components (sheets & FAB) remain the same */}
|
{/* Template Details with tabs */}
|
||||||
{selectedTemplate && (
|
{selectedTemplate && (
|
||||||
<TemplateDetails
|
<TemplateDetails
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
|
@ -1,18 +1,103 @@
|
|||||||
// app/(tabs)/profile.tsx
|
// app/(tabs)/profile.tsx
|
||||||
import { View, ScrollView } from 'react-native';
|
import { View, ScrollView, ImageBackground } from 'react-native';
|
||||||
import { Settings } from 'lucide-react-native';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Settings, LogIn } from 'lucide-react-native';
|
||||||
import { H1 } from '@/components/ui/typography';
|
import { H1 } from '@/components/ui/typography';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { TabScreen } from '@/components/layout/TabScreen';
|
import { TabScreen } from '@/components/layout/TabScreen';
|
||||||
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
const PLACEHOLDER_IMAGE = 'https://github.com/shadcn.png';
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||||
|
import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { currentUser, isAuthenticated } = useNDKCurrentUser();
|
||||||
|
const [profileImageUrl, setProfileImageUrl] = useState<string | undefined>(undefined);
|
||||||
|
const [bannerImageUrl, setBannerImageUrl] = useState<string | undefined>(undefined);
|
||||||
|
const [aboutText, setAboutText] = useState<string | undefined>(undefined);
|
||||||
|
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
||||||
|
|
||||||
|
const displayName = isAuthenticated
|
||||||
|
? (currentUser?.profile?.displayName || currentUser?.profile?.name || 'Nostr User')
|
||||||
|
: 'Guest User';
|
||||||
|
|
||||||
|
const username = isAuthenticated
|
||||||
|
? (currentUser?.profile?.nip05 || '@user')
|
||||||
|
: '@guest';
|
||||||
|
|
||||||
|
// Reset profile data when authentication state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
setProfileImageUrl(undefined);
|
||||||
|
setBannerImageUrl(undefined);
|
||||||
|
setAboutText(undefined);
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
// Extract profile data from Nostr profile
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.profile) {
|
||||||
|
console.log('Profile data:', currentUser.profile);
|
||||||
|
|
||||||
|
// Extract image URL
|
||||||
|
const imageUrl = currentUser.profile.image ||
|
||||||
|
currentUser.profile.picture ||
|
||||||
|
(currentUser.profile as any).avatar ||
|
||||||
|
undefined;
|
||||||
|
setProfileImageUrl(imageUrl);
|
||||||
|
|
||||||
|
// Extract banner URL
|
||||||
|
const bannerUrl = currentUser.profile.banner ||
|
||||||
|
(currentUser.profile as any).background ||
|
||||||
|
undefined;
|
||||||
|
setBannerImageUrl(bannerUrl);
|
||||||
|
|
||||||
|
// Extract about text
|
||||||
|
const about = currentUser.profile.about ||
|
||||||
|
(currentUser.profile as any).description ||
|
||||||
|
undefined;
|
||||||
|
setAboutText(about);
|
||||||
|
}
|
||||||
|
}, [currentUser?.profile]);
|
||||||
|
|
||||||
|
// Show different UI when not authenticated
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<TabScreen>
|
||||||
|
<Header title="Profile" />
|
||||||
|
<View className="flex-1 items-center justify-center p-6">
|
||||||
|
<View className="items-center mb-8">
|
||||||
|
<UserAvatar
|
||||||
|
size="xl"
|
||||||
|
fallback="G"
|
||||||
|
className="mb-4"
|
||||||
|
isInteractive={false}
|
||||||
|
/>
|
||||||
|
<H1 className="text-xl font-semibold mb-1">Guest User</H1>
|
||||||
|
<Text className="text-muted-foreground mb-6">Not logged in</Text>
|
||||||
|
<Text className="text-center text-muted-foreground mb-8">
|
||||||
|
Login with your Nostr private key to view and manage your profile.
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
onPress={() => setIsLoginSheetOpen(true)}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
<Text>Login with Nostr</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* NostrLoginSheet */}
|
||||||
|
<NostrLoginSheet
|
||||||
|
open={isLoginSheetOpen}
|
||||||
|
onClose={() => setIsLoginSheetOpen(false)}
|
||||||
|
/>
|
||||||
|
</TabScreen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabScreen>
|
<TabScreen>
|
||||||
@ -35,19 +120,43 @@ export default function ProfileScreen() {
|
|||||||
paddingBottom: insets.bottom + 20
|
paddingBottom: insets.bottom + 20
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Profile content remains the same */}
|
{/* Banner Image */}
|
||||||
<View className="items-center pt-6 pb-8">
|
<View className="w-full h-40 relative">
|
||||||
<Avatar className="w-24 h-24 mb-4" alt="Profile picture">
|
{bannerImageUrl ? (
|
||||||
<AvatarImage source={{ uri: PLACEHOLDER_IMAGE }} />
|
<ImageBackground
|
||||||
<AvatarFallback>
|
source={{ uri: bannerImageUrl }}
|
||||||
<Text className="text-2xl">JD</Text>
|
className="w-full h-full"
|
||||||
</AvatarFallback>
|
resizeMode="cover"
|
||||||
</Avatar>
|
>
|
||||||
<H1 className="text-xl font-semibold mb-1">John Doe</H1>
|
<View className="absolute inset-0 bg-black/20" />
|
||||||
<Text className="text-muted-foreground">@johndoe</Text>
|
</ImageBackground>
|
||||||
|
) : (
|
||||||
|
<View className="w-full h-full bg-accent" />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex-row justify-around px-4 py-6 bg-card">
|
{/* Profile Avatar and Name - positioned to overlap the banner */}
|
||||||
|
<View className="items-center -mt-16 pb-6">
|
||||||
|
<UserAvatar
|
||||||
|
size="xl"
|
||||||
|
uri={profileImageUrl}
|
||||||
|
fallback={displayName.charAt(0)}
|
||||||
|
className="mb-4 border-4 border-background"
|
||||||
|
isInteractive={false}
|
||||||
|
/>
|
||||||
|
<H1 className="text-xl font-semibold mb-1">{displayName}</H1>
|
||||||
|
<Text className="text-muted-foreground mb-4">{username}</Text>
|
||||||
|
|
||||||
|
{/* About section */}
|
||||||
|
{aboutText && (
|
||||||
|
<View className="px-6 py-2 w-full">
|
||||||
|
<Text className="text-center text-foreground">{aboutText}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<View className="flex-row justify-around px-4 py-6 bg-card mx-4 rounded-lg">
|
||||||
<View className="items-center">
|
<View className="items-center">
|
||||||
<Text className="text-2xl font-bold">24</Text>
|
<Text className="text-2xl font-bold">24</Text>
|
||||||
<Text className="text-muted-foreground">Workouts</Text>
|
<Text className="text-muted-foreground">Workouts</Text>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// app/_layout.tsx
|
||||||
import '@/global.css';
|
import '@/global.css';
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
@ -11,6 +12,9 @@ import { setAndroidNavigationBar } from '@/lib/android-navigation-bar';
|
|||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { DatabaseProvider } from '@/components/DatabaseProvider';
|
import { DatabaseProvider } from '@/components/DatabaseProvider';
|
||||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||||
|
import { SettingsDrawerProvider } from '@/lib/contexts/SettingsDrawerContext';
|
||||||
|
import SettingsDrawer from '@/components/SettingsDrawer';
|
||||||
|
import { useNDKStore } from '@/lib/stores/ndk';
|
||||||
|
|
||||||
const LIGHT_THEME = {
|
const LIGHT_THEME = {
|
||||||
...DefaultTheme,
|
...DefaultTheme,
|
||||||
@ -25,21 +29,26 @@ const DARK_THEME = {
|
|||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const [isInitialized, setIsInitialized] = React.useState(false);
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||||
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
const { colorScheme, isDarkColorScheme } = useColorScheme();
|
||||||
|
const { init } = useNDKStore();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function init() {
|
async function initApp() {
|
||||||
try {
|
try {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
document.documentElement.classList.add('bg-background');
|
document.documentElement.classList.add('bg-background');
|
||||||
}
|
}
|
||||||
setAndroidNavigationBar(colorScheme);
|
setAndroidNavigationBar(colorScheme);
|
||||||
|
|
||||||
|
// Initialize NDK
|
||||||
|
await init();
|
||||||
|
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize:', error);
|
console.error('Failed to initialize:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
initApp();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
@ -55,6 +64,7 @@ export default function RootLayout() {
|
|||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<DatabaseProvider>
|
<DatabaseProvider>
|
||||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||||
|
<SettingsDrawerProvider>
|
||||||
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
|
<StatusBar style={isDarkColorScheme ? 'light' : 'dark'} />
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -64,7 +74,12 @@ export default function RootLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{/* Settings drawer needs to be outside the navigation stack */}
|
||||||
|
<SettingsDrawer />
|
||||||
|
|
||||||
<PortalHost />
|
<PortalHost />
|
||||||
|
</SettingsDrawerProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</DatabaseProvider>
|
</DatabaseProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// components/DatabaseProvider.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, ActivityIndicator, Text } from 'react-native';
|
import { View, ActivityIndicator, Text } from 'react-native';
|
||||||
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
|
||||||
@ -13,6 +12,7 @@ interface DatabaseServicesContextValue {
|
|||||||
exerciseService: ExerciseService | null;
|
exerciseService: ExerciseService | null;
|
||||||
eventCache: EventCache | null;
|
eventCache: EventCache | null;
|
||||||
devSeeder: DevSeederService | null;
|
devSeeder: DevSeederService | null;
|
||||||
|
// Remove NostrService since we're using the hooks-based approach now
|
||||||
}
|
}
|
||||||
|
|
||||||
const DatabaseServicesContext = React.createContext<DatabaseServicesContextValue>({
|
const DatabaseServicesContext = React.createContext<DatabaseServicesContextValue>({
|
||||||
|
@ -1,36 +1,106 @@
|
|||||||
// components/Header.tsx
|
// components/Header.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Platform } from 'react-native';
|
import { View, StyleSheet, Platform } from 'react-native';
|
||||||
import { Text } from '@/components/ui/text';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { Bell } from 'lucide-react-native';
|
||||||
|
import { Text } from '@/components/ui/text';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
|
import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext';
|
||||||
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title?: string;
|
||||||
|
hideTitle?: boolean;
|
||||||
rightElement?: React.ReactNode;
|
rightElement?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ title, rightElement, children }: HeaderProps) {
|
export default function Header({
|
||||||
|
title,
|
||||||
|
hideTitle = false,
|
||||||
|
rightElement
|
||||||
|
}: HeaderProps) {
|
||||||
|
const theme = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const { openDrawer } = useSettingsDrawer();
|
||||||
|
const { currentUser, isAuthenticated } = useNDKCurrentUser();
|
||||||
|
|
||||||
|
// Extract profile image URL
|
||||||
|
const profileImageUrl = currentUser?.profile?.image;
|
||||||
|
|
||||||
|
// Get first letter of name for fallback
|
||||||
|
const fallbackLetter = isAuthenticated && currentUser?.profile?.name
|
||||||
|
? currentUser.profile.name.charAt(0).toUpperCase()
|
||||||
|
: 'G';
|
||||||
|
|
||||||
|
if (hideTitle) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className="flex-row items-center justify-between bg-card border-b border-border"
|
style={[
|
||||||
style={{
|
styles.header,
|
||||||
paddingTop: Platform.OS === 'ios' ? insets.top : insets.top + 8,
|
{
|
||||||
paddingHorizontal: 16,
|
paddingTop: insets.top,
|
||||||
paddingBottom: 12,
|
backgroundColor: theme.colors.card,
|
||||||
}}
|
borderBottomColor: theme.colors.border
|
||||||
|
}
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<View className="flex-1">
|
<View style={styles.headerContent}>
|
||||||
<Text className="text-2xl font-bold">{title}</Text>
|
{/* Left side - User avatar that opens settings drawer */}
|
||||||
{children}
|
<UserAvatar
|
||||||
</View>
|
size="sm"
|
||||||
{rightElement && (
|
uri={profileImageUrl}
|
||||||
<View className="ml-4">
|
onPress={openDrawer}
|
||||||
{rightElement}
|
fallback={fallbackLetter}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Middle - Title */}
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Right side - Custom element or default notifications */}
|
||||||
|
<View style={styles.rightContainer}>
|
||||||
|
{rightElement || (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onPress={() => {}}
|
||||||
|
>
|
||||||
|
<Bell size={24} className="text-foreground" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
header: {
|
||||||
|
width: '100%',
|
||||||
|
borderBottomWidth: Platform.OS === 'ios' ? 0.5 : 1,
|
||||||
|
},
|
||||||
|
headerContent: {
|
||||||
|
height: 60,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
titleContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
rightContainer: {
|
||||||
|
minWidth: 40,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
});
|
414
components/SettingsDrawer.tsx
Normal file
414
components/SettingsDrawer.tsx
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
// components/SettingsDrawer.tsx
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { View, StyleSheet, Animated, Dimensions, ScrollView, Pressable, Platform, TouchableOpacity } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useTheme } from '@react-navigation/native';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import { useSettingsDrawer } from '@/lib/contexts/SettingsDrawerContext';
|
||||||
|
import {
|
||||||
|
Moon, Sun, LogOut, User, ChevronRight, X, Bell, HelpCircle,
|
||||||
|
Smartphone, Database, Zap, RefreshCw, AlertTriangle
|
||||||
|
} from 'lucide-react-native';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Text } from '@/components/ui/text';
|
||||||
|
import { useColorScheme } from '@/lib/useColorScheme';
|
||||||
|
import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
||||||
|
import { useNDKCurrentUser, useNDKAuth } from '@/lib/hooks/useNDK';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||||
|
const DRAWER_WIDTH = SCREEN_WIDTH * 0.85;
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
id: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
rightElement?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsDrawer() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { isDrawerOpen, closeDrawer } = useSettingsDrawer();
|
||||||
|
const { currentUser, isAuthenticated } = useNDKCurrentUser();
|
||||||
|
const { logout } = useNDKAuth();
|
||||||
|
const { toggleColorScheme, isDarkColorScheme } = useColorScheme();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
||||||
|
const [showSignOutAlert, setShowSignOutAlert] = useState(false);
|
||||||
|
|
||||||
|
const slideAnim = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
// Handle drawer animation when open state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDrawerOpen) {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.spring(slideAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
useNativeDriver: true,
|
||||||
|
speed: 20,
|
||||||
|
bounciness: 4
|
||||||
|
}),
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: true
|
||||||
|
})
|
||||||
|
]).start();
|
||||||
|
} else {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.spring(slideAnim, {
|
||||||
|
toValue: -DRAWER_WIDTH,
|
||||||
|
useNativeDriver: true,
|
||||||
|
speed: 20,
|
||||||
|
bounciness: 4
|
||||||
|
}),
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: true
|
||||||
|
})
|
||||||
|
]).start();
|
||||||
|
}
|
||||||
|
}, [isDrawerOpen]);
|
||||||
|
|
||||||
|
// Navigate to relevant screen for login
|
||||||
|
const navigateToLogin = () => {
|
||||||
|
// Go to the profile tab which should have login functionality
|
||||||
|
closeDrawer();
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/(tabs)/profile");
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle profile click - different behavior on iOS vs Android
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
// On iOS, use the sheet directly
|
||||||
|
setIsLoginSheetOpen(true);
|
||||||
|
} else {
|
||||||
|
// On Android, navigate to profile tab
|
||||||
|
navigateToLogin();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Navigate to profile edit screen in the future
|
||||||
|
console.log('Navigate to profile edit');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle sign out button click
|
||||||
|
const handleSignOut = () => {
|
||||||
|
setShowSignOutAlert(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle confirmed sign out
|
||||||
|
const confirmSignOut = async () => {
|
||||||
|
await logout();
|
||||||
|
closeDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nostr integration handler
|
||||||
|
const handleNostrIntegration = () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
// On iOS, use the sheet directly
|
||||||
|
setIsLoginSheetOpen(true);
|
||||||
|
} else {
|
||||||
|
// On Android, navigate to profile tab
|
||||||
|
navigateToLogin();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show Nostr settings in the future
|
||||||
|
console.log('Show Nostr settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define menu items
|
||||||
|
const menuItems: MenuItem[] = [
|
||||||
|
{
|
||||||
|
id: 'appearance',
|
||||||
|
icon: isDarkColorScheme ? Moon : Sun,
|
||||||
|
label: 'Dark Mode',
|
||||||
|
onPress: () => {},
|
||||||
|
rightElement: (
|
||||||
|
<Switch
|
||||||
|
checked={isDarkColorScheme}
|
||||||
|
onCheckedChange={toggleColorScheme}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notifications',
|
||||||
|
icon: Bell,
|
||||||
|
label: 'Notifications',
|
||||||
|
onPress: () => closeDrawer(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data-sync',
|
||||||
|
icon: RefreshCw,
|
||||||
|
label: 'Data Sync',
|
||||||
|
onPress: () => closeDrawer(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'backup-restore',
|
||||||
|
icon: Database,
|
||||||
|
label: 'Backup & Restore',
|
||||||
|
onPress: () => closeDrawer(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device',
|
||||||
|
icon: Smartphone,
|
||||||
|
label: 'Device Settings',
|
||||||
|
onPress: () => closeDrawer(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nostr',
|
||||||
|
icon: Zap,
|
||||||
|
label: 'Nostr Integration',
|
||||||
|
onPress: handleNostrIntegration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'about',
|
||||||
|
icon: HelpCircle,
|
||||||
|
label: 'About',
|
||||||
|
onPress: () => closeDrawer(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isDrawerOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View style={StyleSheet.absoluteFill}>
|
||||||
|
{/* Backdrop overlay */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
StyleSheet.absoluteFill,
|
||||||
|
{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
opacity: fadeAnim
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
onPress={closeDrawer}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.drawer,
|
||||||
|
{
|
||||||
|
transform: [{ translateX: slideAnim }],
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
borderRightColor: theme.colors.border,
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
|
||||||
|
{/* Header with close button */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text className="text-xl font-semibold">Settings</Text>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onPress={closeDrawer}
|
||||||
|
className="absolute right-4"
|
||||||
|
>
|
||||||
|
<X size={24} className="text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Profile section - make it touchable */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.profileSection, { borderBottomColor: theme.colors.border }]}
|
||||||
|
onPress={handleProfileClick}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
alt={currentUser?.profile?.name || "User profile"}
|
||||||
|
className="h-16 w-16"
|
||||||
|
>
|
||||||
|
{isAuthenticated && currentUser?.profile?.image ? (
|
||||||
|
<AvatarImage source={{ uri: currentUser.profile.image }} />
|
||||||
|
) : null}
|
||||||
|
<AvatarFallback>
|
||||||
|
{isAuthenticated && currentUser?.profile?.name ? (
|
||||||
|
<Text className="text-foreground">
|
||||||
|
{currentUser.profile.name.charAt(0).toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<User size={28} />
|
||||||
|
)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<View style={styles.profileInfo}>
|
||||||
|
<Text className="text-lg font-semibold">
|
||||||
|
{isAuthenticated ? currentUser?.profile?.name || 'Nostr User' : 'Not Logged In'}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-muted-foreground">
|
||||||
|
{isAuthenticated ? 'Edit Profile' : 'Login with Nostr'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<ChevronRight size={20} color={theme.colors.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Menu items */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.menuList}
|
||||||
|
contentContainerStyle={styles.menuContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<View key={item.id}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.menuItem}
|
||||||
|
onPress={item.onPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.menuItemLeft}>
|
||||||
|
<item.icon size={22} color={theme.colors.text} />
|
||||||
|
<Text className="text-base ml-3">{item.label}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.rightElement ? (
|
||||||
|
item.rightElement
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={20} color={theme.colors.text} />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{index < menuItems.length - 1 && (
|
||||||
|
<Separator className="mb-1 mt-1" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Sign out button at the bottom - only show when authenticated */}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
onPress={handleSignOut}
|
||||||
|
>
|
||||||
|
<Text className="text-destructive-foreground">Sign Out</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Only render the NostrLoginSheet on iOS */}
|
||||||
|
{Platform.OS === 'ios' && (
|
||||||
|
<NostrLoginSheet
|
||||||
|
open={isLoginSheetOpen}
|
||||||
|
onClose={() => setIsLoginSheetOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sign Out Alert Dialog */}
|
||||||
|
<AlertDialog open={showSignOutAlert} onOpenChange={setShowSignOutAlert}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>For Real?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Text>
|
||||||
|
Are you sure you want to sign out? Make sure you've backed up your private key.
|
||||||
|
Lost keys cannot be recovered and all your data will be inaccessible.
|
||||||
|
</Text>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onPress={() => setShowSignOutAlert(false)}>
|
||||||
|
<Text>Cancel</Text>
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onPress={confirmSignOut}
|
||||||
|
className="bg-destructive text-destructive-foreground"
|
||||||
|
>
|
||||||
|
<Text>Sign Out</Text>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
drawer: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 3.84,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
|
profileSection: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
profileInfo: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
menuList: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
menuContent: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
menuItemLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: Platform.OS === 'ios' ? 16 : 16,
|
||||||
|
},
|
||||||
|
});
|
94
components/UserAvatar.tsx
Normal file
94
components/UserAvatar.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// components/UserAvatar.tsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { TouchableOpacity, TouchableOpacityProps, GestureResponderEvent } from 'react-native';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { Text } from '@/components/ui/text';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface UserAvatarProps extends TouchableOpacityProps {
|
||||||
|
uri?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
fallback?: string;
|
||||||
|
isInteractive?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAvatar = ({
|
||||||
|
uri,
|
||||||
|
size = 'md',
|
||||||
|
fallback = 'U',
|
||||||
|
isInteractive = true,
|
||||||
|
className,
|
||||||
|
onPress,
|
||||||
|
...props
|
||||||
|
}: UserAvatarProps) => {
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [imageUri, setImageUri] = useState<string | undefined>(uri);
|
||||||
|
|
||||||
|
// Update imageUri when uri prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
setImageUri(uri);
|
||||||
|
setImageError(false);
|
||||||
|
}, [uri]);
|
||||||
|
|
||||||
|
// Log the URI for debugging
|
||||||
|
console.log("Avatar URI:", uri);
|
||||||
|
|
||||||
|
const containerStyles = cn(
|
||||||
|
{
|
||||||
|
'w-8 h-8': size === 'sm',
|
||||||
|
'w-10 h-10': size === 'md',
|
||||||
|
'w-12 h-12': size === 'lg',
|
||||||
|
'w-24 h-24': size === 'xl',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePress = (event: GestureResponderEvent) => {
|
||||||
|
if (onPress) {
|
||||||
|
onPress(event);
|
||||||
|
} else if (isInteractive) {
|
||||||
|
// Default behavior if no onPress provided
|
||||||
|
console.log('Avatar pressed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
console.error("Failed to load image from URI:", imageUri);
|
||||||
|
setImageError(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarContent = (
|
||||||
|
<Avatar
|
||||||
|
className={containerStyles}
|
||||||
|
alt="User profile image"
|
||||||
|
>
|
||||||
|
{imageUri && !imageError ? (
|
||||||
|
<AvatarImage
|
||||||
|
source={{ uri: imageUri }}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AvatarFallback>
|
||||||
|
<Text className="text-foreground">{fallback}</Text>
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInteractive) return avatarContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handlePress}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="User profile"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{avatarContent}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserAvatar;
|
110
components/sheets/NostrLoginSheet.tsx
Normal file
110
components/sheets/NostrLoginSheet.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// components/sheets/NostrLoginSheet.tsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, StyleSheet, Alert, Platform, KeyboardAvoidingView, ScrollView } from 'react-native';
|
||||||
|
import { Info, ArrowRight } from 'lucide-react-native';
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
|
import { Text } from '@/components/ui/text';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { useNDKAuth } from '@/lib/hooks/useNDK';
|
||||||
|
|
||||||
|
interface NostrLoginSheetProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NostrLoginSheet({ open, onClose }: NostrLoginSheetProps) {
|
||||||
|
const [privateKey, setPrivateKey] = useState('');
|
||||||
|
const { login, isLoading } = useNDKAuth();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!privateKey.trim()) {
|
||||||
|
Alert.alert('Error', 'Please enter your private key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await login(privateKey);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setPrivateKey('');
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
Alert.alert('Login Error', 'Failed to login with the provided private key');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
Alert.alert('Error', 'An unexpected error occurred');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
isOpen={open}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<SheetContent>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Login with Nostr</SheetTitle>
|
||||||
|
{/* Removed the X close button here */}
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<ScrollView style={styles.scrollView}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<Text className="mb-2">Enter your Nostr private key</Text>
|
||||||
|
<Input
|
||||||
|
placeholder="nsec1..."
|
||||||
|
value={privateKey}
|
||||||
|
onChangeText={setPrivateKey}
|
||||||
|
secureTextEntry
|
||||||
|
autoCapitalize="none"
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={handleLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full mb-6"
|
||||||
|
>
|
||||||
|
<Text>{isLoading ? 'Logging in...' : 'Login'}</Text>
|
||||||
|
{!isLoading}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Separator className="mb-4" />
|
||||||
|
|
||||||
|
<View className="bg-secondary/30 p-3 rounded-md">
|
||||||
|
<View className="flex-row items-center mb-2">
|
||||||
|
<Info size={16} className="mr-3 text-muted-foreground" />
|
||||||
|
<Text className="font-semibold">What is a Nostr Key?</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-sm text-muted-foreground mb-2">
|
||||||
|
Nostr is a decentralized protocol where your private key (nsec) is your identity and password.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-muted-foreground">
|
||||||
|
Your private key is securely stored on your device and is never sent to any servers.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
});
|
@ -17,7 +17,6 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
|
||||||
import { Template, TemplateExerciseDisplay } from '@/types/templates';
|
import { Template, TemplateExerciseDisplay } from '@/types/templates';
|
||||||
|
|
||||||
interface TemplateCardProps {
|
interface TemplateCardProps {
|
||||||
@ -35,8 +34,8 @@ export function TemplateCard({
|
|||||||
onFavorite,
|
onFavorite,
|
||||||
onStartWorkout
|
onStartWorkout
|
||||||
}: TemplateCardProps) {
|
}: TemplateCardProps) {
|
||||||
const [showSheet, setShowSheet] = React.useState(false);
|
|
||||||
const [showDeleteAlert, setShowDeleteAlert] = React.useState(false);
|
const [showDeleteAlert, setShowDeleteAlert] = React.useState(false);
|
||||||
|
const lastUsed = template.metadata?.lastUsed ? new Date(template.metadata.lastUsed) : undefined;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@ -47,25 +46,16 @@ export function TemplateCard({
|
|||||||
description,
|
description,
|
||||||
tags = [],
|
tags = [],
|
||||||
source,
|
source,
|
||||||
metadata,
|
|
||||||
isFavorite
|
isFavorite
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
const lastUsed = metadata?.lastUsed ? new Date(metadata.lastUsed) : undefined;
|
|
||||||
|
|
||||||
const handleConfirmDelete = () => {
|
const handleConfirmDelete = () => {
|
||||||
onDelete(id);
|
onDelete(id);
|
||||||
setShowDeleteAlert(false);
|
setShowDeleteAlert(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardPress = () => {
|
|
||||||
setShowSheet(true);
|
|
||||||
onPress();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||||
<TouchableOpacity onPress={handleCardPress} activeOpacity={0.7}>
|
|
||||||
<Card className="mx-4">
|
<Card className="mx-4">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<View className="flex-row justify-between items-start">
|
<View className="flex-row justify-between items-start">
|
||||||
@ -97,7 +87,7 @@ export function TemplateCard({
|
|||||||
Exercises:
|
Exercises:
|
||||||
</Text>
|
</Text>
|
||||||
<View className="gap-1">
|
<View className="gap-1">
|
||||||
{exercises.slice(0, 3).map((exercise: TemplateExerciseDisplay, index: number) => (
|
{exercises.slice(0, 3).map((exercise, index) => (
|
||||||
<Text key={index} className="text-sm text-muted-foreground">
|
<Text key={index} className="text-sm text-muted-foreground">
|
||||||
• {exercise.title} ({exercise.targetSets}×{exercise.targetReps})
|
• {exercise.title} ({exercise.targetSets}×{exercise.targetReps})
|
||||||
</Text>
|
</Text>
|
||||||
@ -119,7 +109,7 @@ export function TemplateCard({
|
|||||||
|
|
||||||
{tags.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<View className="flex-row flex-wrap gap-2 mt-2">
|
<View className="flex-row flex-wrap gap-2 mt-2">
|
||||||
{tags.map((tag: string) => (
|
{tags.map(tag => (
|
||||||
<Badge key={tag} variant="outline" className="text-xs">
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
<Text>{tag}</Text>
|
<Text>{tag}</Text>
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -140,6 +130,7 @@ export function TemplateCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onPress={onStartWorkout}
|
onPress={onStartWorkout}
|
||||||
className="native:h-10 native:w-10"
|
className="native:h-10 native:w-10"
|
||||||
|
accessibilityLabel="Start workout"
|
||||||
>
|
>
|
||||||
<Play className="text-primary" size={20} />
|
<Play className="text-primary" size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
@ -148,6 +139,7 @@ export function TemplateCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onPress={onFavorite}
|
onPress={onFavorite}
|
||||||
className="native:h-10 native:w-10"
|
className="native:h-10 native:w-10"
|
||||||
|
accessibilityLabel={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
||||||
>
|
>
|
||||||
<Star
|
<Star
|
||||||
className={isFavorite ? "text-primary" : "text-muted-foreground"}
|
className={isFavorite ? "text-primary" : "text-muted-foreground"}
|
||||||
@ -161,6 +153,7 @@ export function TemplateCard({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="native:h-10 native:w-10 native:bg-muted/50 items-center justify-center"
|
className="native:h-10 native:w-10 native:bg-muted/50 items-center justify-center"
|
||||||
|
accessibilityLabel="Delete template"
|
||||||
>
|
>
|
||||||
<Trash2
|
<Trash2
|
||||||
size={20}
|
size={20}
|
||||||
@ -196,58 +189,5 @@ export function TemplateCard({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Sheet for detailed view */}
|
|
||||||
<Sheet isOpen={showSheet} onClose={() => setShowSheet(false)}>
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>
|
|
||||||
<Text className="text-xl font-bold">{title}</Text>
|
|
||||||
</SheetTitle>
|
|
||||||
</SheetHeader>
|
|
||||||
<SheetContent>
|
|
||||||
<View className="gap-6">
|
|
||||||
{description && (
|
|
||||||
<View>
|
|
||||||
<Text className="text-base font-semibold mb-2">Description</Text>
|
|
||||||
<Text className="text-base leading-relaxed">{description}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View>
|
|
||||||
<Text className="text-base font-semibold mb-2">Details</Text>
|
|
||||||
<View className="gap-2">
|
|
||||||
<Text className="text-base">Type: {type}</Text>
|
|
||||||
<Text className="text-base">Category: {category}</Text>
|
|
||||||
<Text className="text-base">Source: {source}</Text>
|
|
||||||
{metadata?.useCount && (
|
|
||||||
<Text className="text-base">Times Used: {metadata.useCount}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<Text className="text-base font-semibold mb-2">Exercises</Text>
|
|
||||||
<View className="gap-2">
|
|
||||||
{exercises.map((exercise: TemplateExerciseDisplay, index: number) => (
|
|
||||||
<Text key={index} className="text-base">
|
|
||||||
{exercise.title} ({exercise.targetSets}×{exercise.targetReps})
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{tags.length > 0 && (
|
|
||||||
<View>
|
|
||||||
<Text className="text-base font-semibold mb-2">Tags</Text>
|
|
||||||
<View className="flex-row flex-wrap gap-2">
|
|
||||||
{tags.map((tag: string) => (
|
|
||||||
<Badge key={tag} variant="secondary">
|
|
||||||
<Text>{tag}</Text>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -14,13 +14,10 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Hash,
|
Hash,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Settings
|
Settings,
|
||||||
|
LineChart
|
||||||
} from 'lucide-react-native';
|
} from 'lucide-react-native';
|
||||||
import {
|
import { WorkoutTemplate, getSourceDisplay } from '@/types/templates';
|
||||||
WorkoutTemplate,
|
|
||||||
TemplateSource,
|
|
||||||
getSourceDisplay
|
|
||||||
} from '@/types/templates';
|
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { useTheme } from '@react-navigation/native';
|
||||||
import type { CustomTheme } from '@/lib/theme';
|
import type { CustomTheme } from '@/lib/theme';
|
||||||
|
|
||||||
@ -43,7 +40,7 @@ function OverviewTab({ template, onEdit }: { template: WorkoutTemplate; onEdit?:
|
|||||||
exercises = [],
|
exercises = [],
|
||||||
tags = [],
|
tags = [],
|
||||||
metadata,
|
metadata,
|
||||||
availability // Replace source with availability
|
availability
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
// Calculate source type from availability
|
// Calculate source type from availability
|
||||||
@ -182,19 +179,6 @@ function OverviewTab({ template, onEdit }: { template: WorkoutTemplate; onEdit?:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to format template source for display
|
|
||||||
function formatTemplateSource(source: TemplateSource | undefined, templateSource: 'local' | 'powr' | 'nostr'): string {
|
|
||||||
if (!source) {
|
|
||||||
return templateSource === 'local' ? 'Local Template' : templateSource.toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = source.authorName || 'Unknown Author';
|
|
||||||
if (source.version) {
|
|
||||||
return `Modified from ${author} (v${source.version})`;
|
|
||||||
}
|
|
||||||
return `Original by ${author}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// History Tab Component
|
// History Tab Component
|
||||||
function HistoryTab({ template }: { template: WorkoutTemplate }) {
|
function HistoryTab({ template }: { template: WorkoutTemplate }) {
|
||||||
return (
|
return (
|
||||||
@ -206,22 +190,44 @@ function HistoryTab({ template }: { template: WorkoutTemplate }) {
|
|||||||
{/* Performance Stats */}
|
{/* Performance Stats */}
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-base font-semibold text-foreground mb-4">Performance Summary</Text>
|
<Text className="text-base font-semibold text-foreground mb-4">Performance Summary</Text>
|
||||||
<View className="flex-row gap-4">
|
<View className="gap-4">
|
||||||
<View className="flex-1 bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Avg. Duration</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Usage Stats</Text>
|
||||||
|
<View className="flex-row justify-between mt-2">
|
||||||
|
<View>
|
||||||
|
<Text className="text-sm text-muted-foreground">Total Workouts</Text>
|
||||||
|
<Text className="text-lg font-semibold text-foreground">
|
||||||
|
{template.metadata?.useCount || 0}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text className="text-sm text-muted-foreground">Avg. Duration</Text>
|
||||||
<Text className="text-lg font-semibold text-foreground">
|
<Text className="text-lg font-semibold text-foreground">
|
||||||
{template.metadata?.averageDuration
|
{template.metadata?.averageDuration
|
||||||
? `${Math.round(template.metadata.averageDuration / 60)}m`
|
? `${Math.round(template.metadata.averageDuration / 60)}m`
|
||||||
: '--'}
|
: '--'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1 bg-card p-4 rounded-lg">
|
<View>
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Completion Rate</Text>
|
<Text className="text-sm text-muted-foreground">Completion Rate</Text>
|
||||||
<Text className="text-lg font-semibold text-foreground">--</Text>
|
<Text className="text-lg font-semibold text-foreground">--</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Progress Chart Placeholder */}
|
||||||
|
<View className="bg-card p-4 rounded-lg">
|
||||||
|
<Text className="text-sm text-muted-foreground mb-4">Progress Over Time</Text>
|
||||||
|
<View className="h-40 items-center justify-center">
|
||||||
|
<LineChart size={24} className="text-muted-foreground mb-2" />
|
||||||
|
<Text className="text-sm text-muted-foreground">
|
||||||
|
Progress tracking coming soon
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* History List */}
|
{/* History List */}
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-base font-semibold text-foreground mb-4">Recent Workouts</Text>
|
<Text className="text-base font-semibold text-foreground mb-4">Recent Workouts</Text>
|
||||||
@ -232,6 +238,9 @@ function HistoryTab({ template }: { template: WorkoutTemplate }) {
|
|||||||
<Text className="text-muted-foreground text-center">
|
<Text className="text-muted-foreground text-center">
|
||||||
No workout history available yet
|
No workout history available yet
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text className="text-sm text-muted-foreground text-center mt-1">
|
||||||
|
Complete a workout using this template to see your history
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -250,6 +259,13 @@ function SettingsTab({ template }: { template: WorkoutTemplate }) {
|
|||||||
restBetweenRounds,
|
restBetweenRounds,
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
|
// Helper function to format seconds into MM:SS
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
className="flex-1 px-4"
|
className="flex-1 px-4"
|
||||||
@ -261,31 +277,31 @@ function SettingsTab({ template }: { template: WorkoutTemplate }) {
|
|||||||
<Text className="text-base font-semibold text-foreground mb-4">Workout Settings</Text>
|
<Text className="text-base font-semibold text-foreground mb-4">Workout Settings</Text>
|
||||||
<View className="gap-4">
|
<View className="gap-4">
|
||||||
<View className="bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Type</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Workout Type</Text>
|
||||||
<Text className="text-base font-medium text-foreground capitalize">{type}</Text>
|
<Text className="text-base font-medium text-foreground capitalize">{type}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{rounds && (
|
{rounds && (
|
||||||
<View className="bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Rounds</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Number of Rounds</Text>
|
||||||
<Text className="text-base font-medium text-foreground">{rounds}</Text>
|
<Text className="text-base font-medium text-foreground">{rounds}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{duration && (
|
{duration && (
|
||||||
<View className="bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Duration</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Total Duration</Text>
|
||||||
<Text className="text-base font-medium text-foreground">
|
<Text className="text-base font-medium text-foreground">
|
||||||
{Math.floor(duration / 60)}:{(duration % 60).toString().padStart(2, '0')}
|
{formatTime(duration)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{interval && (
|
{interval && (
|
||||||
<View className="bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Interval</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Interval Time</Text>
|
||||||
<Text className="text-base font-medium text-foreground">
|
<Text className="text-base font-medium text-foreground">
|
||||||
{Math.floor(interval / 60)}:{(interval % 60).toString().padStart(2, '0')}
|
{formatTime(interval)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -294,12 +310,23 @@ function SettingsTab({ template }: { template: WorkoutTemplate }) {
|
|||||||
<View className="bg-card p-4 rounded-lg">
|
<View className="bg-card p-4 rounded-lg">
|
||||||
<Text className="text-sm text-muted-foreground mb-1">Rest Between Rounds</Text>
|
<Text className="text-sm text-muted-foreground mb-1">Rest Between Rounds</Text>
|
||||||
<Text className="text-base font-medium text-foreground">
|
<Text className="text-base font-medium text-foreground">
|
||||||
{Math.floor(restBetweenRounds / 60)}:{(restBetweenRounds % 60).toString().padStart(2, '0')}
|
{formatTime(restBetweenRounds)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Sync Settings */}
|
||||||
|
<View>
|
||||||
|
<Text className="text-base font-semibold text-foreground mb-4">Sync Settings</Text>
|
||||||
|
<View className="bg-card p-4 rounded-lg">
|
||||||
|
<Text className="text-sm text-muted-foreground mb-1">Template Source</Text>
|
||||||
|
<Text className="text-base font-medium text-foreground">
|
||||||
|
{getSourceDisplay(template)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
@ -321,7 +348,7 @@ export function TemplateDetails({
|
|||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<SheetContent>
|
<SheetContent>
|
||||||
<View style={{ flex: 1, minHeight: 400 }}>
|
<View style={{ flex: 1, minHeight: 400 }} className="rounded-t-[10px]">
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
2135
docs/design/Settings/SettingsImplementationGuide.md
Normal file
2135
docs/design/Settings/SettingsImplementationGuide.md
Normal file
File diff suppressed because it is too large
Load Diff
204
docs/design/Templates/TemplateOrganization.md
Normal file
204
docs/design/Templates/TemplateOrganization.md
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# Template Organization and Drag-and-Drop Feature
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
Users need a more flexible way to organize their growing collection of workout templates. Currently, templates are organized in a flat list with basic filtering. Users need the ability to create custom folders and easily reorganize templates through drag-and-drop interactions.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- Create, rename, and delete folders
|
||||||
|
- Move templates between folders via drag-and-drop
|
||||||
|
- Reorder templates within folders
|
||||||
|
- Reorder folders themselves
|
||||||
|
- Collapse/expand folder views
|
||||||
|
- Support template search across folders
|
||||||
|
- Maintain existing category and favorite filters
|
||||||
|
- Support template color coding
|
||||||
|
- Batch move/delete operations
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- Smooth drag animations (60fps)
|
||||||
|
- Persist folder structure locally
|
||||||
|
- Support offline operation
|
||||||
|
- Sync with Nostr when available
|
||||||
|
- Maintain current performance with 100+ templates
|
||||||
|
- Accessible drag-and-drop interactions
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### 1. Folder Data Structure
|
||||||
|
Using a hierarchical structure with templates linked to folders:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TemplateFolder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
order: number;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Template {
|
||||||
|
// ... existing fields ...
|
||||||
|
folder_id?: string; // Optional - null means root level
|
||||||
|
order: number; // Position within folder or root
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
- Simple but flexible structure
|
||||||
|
- Easy to query and update
|
||||||
|
- Supports future nested folders if needed
|
||||||
|
- Maintains compatibility with existing template structure
|
||||||
|
|
||||||
|
### 2. Drag-and-Drop Implementation
|
||||||
|
Using react-native-reanimated and react-native-gesture-handler:
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
- Native performance for animations
|
||||||
|
- Built-in gesture handling
|
||||||
|
- Good community support
|
||||||
|
- Cross-platform compatibility
|
||||||
|
- Rich animation capabilities
|
||||||
|
|
||||||
|
## Technical Design
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
```typescript
|
||||||
|
// Folder management
|
||||||
|
interface FolderManagerHook {
|
||||||
|
folders: TemplateFolder[];
|
||||||
|
createFolder: (name: string) => Promise<string>;
|
||||||
|
updateFolder: (id: string, data: Partial<TemplateFolder>) => Promise<void>;
|
||||||
|
deleteFolder: (id: string) => Promise<void>;
|
||||||
|
reorderFolder: (id: string, newOrder: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draggable template component
|
||||||
|
interface DraggableTemplateProps {
|
||||||
|
template: Template;
|
||||||
|
onDragStart?: () => void;
|
||||||
|
onDragEnd?: (dropZone: DropZone) => void;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop zone types
|
||||||
|
type DropZone = {
|
||||||
|
type: 'folder' | 'root' | 'template';
|
||||||
|
id: string;
|
||||||
|
position: 'before' | 'after' | 'inside';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
- SQLite database for local storage
|
||||||
|
- Nostr event kind for folder structure
|
||||||
|
- Template list screen
|
||||||
|
- Template filtering system
|
||||||
|
- Drag animation system
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Foundation
|
||||||
|
1. Update database schema
|
||||||
|
2. Create folder management hooks
|
||||||
|
3. Implement basic folder CRUD operations
|
||||||
|
4. Add folder view to template screen
|
||||||
|
|
||||||
|
### Phase 2: Drag and Drop
|
||||||
|
1. Implement DraggableTemplateCard
|
||||||
|
2. Add drag gesture handling
|
||||||
|
3. Create drop zone detection
|
||||||
|
4. Implement reordering logic
|
||||||
|
|
||||||
|
### Phase 3: Enhancement
|
||||||
|
1. Add folder customization
|
||||||
|
2. Implement batch operations
|
||||||
|
3. Add Nostr sync support
|
||||||
|
4. Polish animations and feedback
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Folder CRUD operations
|
||||||
|
- Template ordering logic
|
||||||
|
- Drop zone detection
|
||||||
|
- Data structure validation
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Drag and drop flows
|
||||||
|
- Folder persistence
|
||||||
|
- Search across folders
|
||||||
|
- Filter interactions
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Folder operations
|
||||||
|
- Drag and drop events
|
||||||
|
- Error conditions
|
||||||
|
- Performance metrics
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
- Folder usage statistics
|
||||||
|
- Common template organizations
|
||||||
|
- Operation success rates
|
||||||
|
- Animation performance
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
### Potential Enhancements
|
||||||
|
- Nested folders
|
||||||
|
- Folder sharing
|
||||||
|
- Template duplicating
|
||||||
|
- Advanced sorting options
|
||||||
|
- Folder templates
|
||||||
|
- Batch operations
|
||||||
|
- Grid view option
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
- Initial complexity increase
|
||||||
|
- Performance with very large template sets
|
||||||
|
- Cross-device sync challenges
|
||||||
|
- Platform-specific gesture differences
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Runtime Dependencies
|
||||||
|
- react-native-reanimated ^3.0.0
|
||||||
|
- react-native-gesture-handler ^2.0.0
|
||||||
|
- SQLite storage
|
||||||
|
- NDK for Nostr sync
|
||||||
|
|
||||||
|
### Development Dependencies
|
||||||
|
- TypeScript
|
||||||
|
- Jest for testing
|
||||||
|
- React Native testing library
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
- Validate folder names
|
||||||
|
- Sanitize template data
|
||||||
|
- Secure local storage
|
||||||
|
- Safe Nostr event handling
|
||||||
|
|
||||||
|
## Rollout Strategy
|
||||||
|
|
||||||
|
### Development Phase
|
||||||
|
1. Implement core folder structure
|
||||||
|
2. Add basic drag-and-drop
|
||||||
|
3. Beta test with power users
|
||||||
|
4. Polish based on feedback
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
1. Feature flag for initial release
|
||||||
|
2. Gradual rollout to users
|
||||||
|
3. Monitor performance metrics
|
||||||
|
4. Collect user feedback
|
||||||
|
|
||||||
|
## References
|
||||||
|
- [React Native Reanimated Documentation](https://docs.swmansion.com/react-native-reanimated/)
|
||||||
|
- [React Native Gesture Handler Documentation](https://docs.swmansion.com/react-native-gesture-handler/)
|
||||||
|
- [SQLite Documentation](https://www.sqlite.org/docs.html)
|
||||||
|
- [Nostr NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
33
lib/contexts/SettingsDrawerContext.tsx
Normal file
33
lib/contexts/SettingsDrawerContext.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// lib/contexts/SettingsDrawerContext.tsx
|
||||||
|
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface SettingsDrawerContextType {
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
openDrawer: () => void;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
toggleDrawer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsDrawerContext = createContext<SettingsDrawerContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function SettingsDrawerProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const openDrawer = () => setIsDrawerOpen(true);
|
||||||
|
const closeDrawer = () => setIsDrawerOpen(false);
|
||||||
|
const toggleDrawer = () => setIsDrawerOpen(prev => !prev);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsDrawerContext.Provider value={{ isDrawerOpen, openDrawer, closeDrawer, toggleDrawer }}>
|
||||||
|
{children}
|
||||||
|
</SettingsDrawerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettingsDrawer(): SettingsDrawerContextType {
|
||||||
|
const context = useContext(SettingsDrawerContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useSettingsDrawer must be used within a SettingsDrawerProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
import { SQLiteDatabase } from 'expo-sqlite';
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export const SCHEMA_VERSION = 3; // Incrementing version for new tables
|
export const SCHEMA_VERSION = 4; // Updated to version 4 for user_profiles table
|
||||||
|
|
||||||
class Schema {
|
class Schema {
|
||||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||||
@ -164,6 +164,52 @@ class Schema {
|
|||||||
console.log('[Schema] Version 3 upgrade completed');
|
console.log('[Schema] Version 3 upgrade completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update to version 4 if needed - User Profiles
|
||||||
|
if (currentVersion < 4) {
|
||||||
|
console.log('[Schema] Upgrading to version 4');
|
||||||
|
|
||||||
|
// Create user profiles table
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_profiles (
|
||||||
|
pubkey TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
display_name TEXT,
|
||||||
|
about TEXT,
|
||||||
|
website TEXT,
|
||||||
|
picture TEXT,
|
||||||
|
nip05 TEXT,
|
||||||
|
lud16 TEXT,
|
||||||
|
last_updated INTEGER
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create index for faster lookup
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_profiles_last_updated
|
||||||
|
ON user_profiles(last_updated DESC);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create user relays table for storing preferred relays
|
||||||
|
await db.execAsync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_relays (
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
relay_url TEXT NOT NULL,
|
||||||
|
read BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
write BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (pubkey, relay_url),
|
||||||
|
FOREIGN KEY(pubkey) REFERENCES user_profiles(pubkey) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.runAsync(
|
||||||
|
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||||
|
[4, Date.now()]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[Schema] Version 4 upgrade completed');
|
||||||
|
}
|
||||||
|
|
||||||
// Verify final schema
|
// Verify final schema
|
||||||
const tables = await db.getAllAsync<{ name: string }>(
|
const tables = await db.getAllAsync<{ name: string }>(
|
||||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||||
|
41
lib/hooks/useNDK.ts
Normal file
41
lib/hooks/useNDK.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// lib/hooks/useNDK.ts
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNDKStore } from '../stores/ndk';
|
||||||
|
import type { NDKUser } from '@nostr-dev-kit/ndk';
|
||||||
|
|
||||||
|
export function useNDK() {
|
||||||
|
const { ndk, isLoading, init } = useNDKStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ndk && !isLoading) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}, [ndk, isLoading, init]);
|
||||||
|
|
||||||
|
return { ndk, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNDKCurrentUser(): {
|
||||||
|
currentUser: NDKUser | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const { currentUser, isAuthenticated, isLoading } = useNDKStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentUser,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNDKAuth() {
|
||||||
|
const { login, logout, isAuthenticated, isLoading } = useNDKStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
68
lib/hooks/useSubscribe.ts
Normal file
68
lib/hooks/useSubscribe.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// lib/hooks/useSubscribe.ts
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||||
|
import { useNDK } from './useNDK';
|
||||||
|
|
||||||
|
interface UseSubscribeOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
closeOnEose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscribe(
|
||||||
|
filters: NDKFilter[] | false,
|
||||||
|
options: UseSubscribeOptions = {}
|
||||||
|
) {
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [eose, setEose] = useState(false);
|
||||||
|
|
||||||
|
// Default options
|
||||||
|
const { enabled = true, closeOnEose = false } = options;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ndk || !filters || !enabled) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setEose(false);
|
||||||
|
|
||||||
|
let subscription: NDKSubscription;
|
||||||
|
|
||||||
|
try {
|
||||||
|
subscription = ndk.subscribe(filters);
|
||||||
|
|
||||||
|
subscription.on('event', (event: NDKEvent) => {
|
||||||
|
setEvents(prev => {
|
||||||
|
// Avoid duplicates
|
||||||
|
if (prev.some(e => e.id === event.id)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, event];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subscription.on('eose', () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setEose(true);
|
||||||
|
|
||||||
|
if (closeOnEose) {
|
||||||
|
subscription.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useSubscribe] Error:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ndk, enabled, closeOnEose, JSON.stringify(filters)]);
|
||||||
|
|
||||||
|
return { events, isLoading, eose };
|
||||||
|
}
|
197
lib/stores/ndk.ts
Normal file
197
lib/stores/ndk.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// lib/stores/ndk.ts
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Constants for SecureStore
|
||||||
|
const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey';
|
||||||
|
|
||||||
|
// Default relays
|
||||||
|
const DEFAULT_RELAYS = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.nostr.band',
|
||||||
|
'wss://purplepag.es',
|
||||||
|
'wss://nos.lol'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function to convert Array/Uint8Array to hex string
|
||||||
|
function arrayToHex(array: number[] | Uint8Array): string {
|
||||||
|
return Array.from(array)
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
type NDKStoreState = {
|
||||||
|
ndk: NDK | null;
|
||||||
|
currentUser: NDKUser | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
init: () => Promise<void>;
|
||||||
|
login: (privateKey: string) => Promise<boolean>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
getPublicKey: () => Promise<string | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNDKStore = create<NDKStoreState>((set, get) => ({
|
||||||
|
ndk: null,
|
||||||
|
currentUser: null,
|
||||||
|
isLoading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
init: async () => {
|
||||||
|
try {
|
||||||
|
console.log('[NDK] Initializing...');
|
||||||
|
// Initialize NDK with relays
|
||||||
|
const ndk = new NDK({
|
||||||
|
explicitRelayUrls: DEFAULT_RELAYS
|
||||||
|
});
|
||||||
|
|
||||||
|
await ndk.connect();
|
||||||
|
set({ ndk });
|
||||||
|
|
||||||
|
// Check for saved private key
|
||||||
|
const privateKey = await SecureStore.getItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
||||||
|
if (privateKey) {
|
||||||
|
console.log('[NDK] Found saved private key, initializing signer');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create signer with private key
|
||||||
|
const signer = new NDKPrivateKeySigner(privateKey);
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
// Get user and profile
|
||||||
|
const user = await ndk.signer.user();
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
console.log('[NDK] User authenticated:', user.pubkey);
|
||||||
|
await user.fetchProfile();
|
||||||
|
set({
|
||||||
|
currentUser: user,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NDK] Error initializing with saved key:', error);
|
||||||
|
// Remove invalid key
|
||||||
|
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NDK] Initialization error:', error);
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// lib/stores/ndk.ts - updated login method
|
||||||
|
login: async (privateKey: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { ndk } = get();
|
||||||
|
if (!ndk) {
|
||||||
|
console.error('[NDK] NDK not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the private key (handle nsec format)
|
||||||
|
let hexKey = privateKey;
|
||||||
|
|
||||||
|
if (privateKey.startsWith('nsec1')) {
|
||||||
|
try {
|
||||||
|
const { type, data } = nip19.decode(privateKey);
|
||||||
|
if (type !== 'nsec') {
|
||||||
|
throw new Error('Invalid nsec key');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different data types
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
hexKey = data;
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
// Convert array to hex string
|
||||||
|
hexKey = arrayToHex(data);
|
||||||
|
} else if (data instanceof Uint8Array) {
|
||||||
|
// Convert Uint8Array to hex string
|
||||||
|
hexKey = arrayToHex(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported key format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NDK] Key decode error:', error);
|
||||||
|
throw new Error('Invalid private key format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create signer with hex key
|
||||||
|
console.log('[NDK] Creating signer with key');
|
||||||
|
const signer = new NDKPrivateKeySigner(hexKey);
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const user = await ndk.signer.user();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Failed to get user from signer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user profile
|
||||||
|
console.log('[NDK] Fetching user profile');
|
||||||
|
await user.fetchProfile();
|
||||||
|
|
||||||
|
// Process profile data to ensure image property is set
|
||||||
|
if (user.profile) {
|
||||||
|
if (!user.profile.image && (user.profile as any).picture) {
|
||||||
|
user.profile.image = (user.profile as any).picture;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[NDK] User profile loaded:', user.profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the private key securely
|
||||||
|
await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, hexKey);
|
||||||
|
|
||||||
|
set({
|
||||||
|
currentUser: user,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NDK] Login error:', error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: async () => {
|
||||||
|
try {
|
||||||
|
// Remove private key from secure storage
|
||||||
|
await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY);
|
||||||
|
|
||||||
|
// Reset NDK state
|
||||||
|
const { ndk } = get();
|
||||||
|
if (ndk) {
|
||||||
|
ndk.signer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completely reset the user state
|
||||||
|
set({
|
||||||
|
currentUser: null,
|
||||||
|
isAuthenticated: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[NDK] User logged out successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NDK] Logout error:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublicKey: async () => {
|
||||||
|
const { currentUser } = get();
|
||||||
|
if (currentUser) {
|
||||||
|
return currentUser.pubkey;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
512
package-lock.json
generated
512
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/cli": "^0.22.16",
|
"@expo/cli": "^0.22.16",
|
||||||
|
"@nostr-dev-kit/ndk": "^2.12.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||||
@ -48,6 +49,7 @@
|
|||||||
"expo-linking": "~7.0.4",
|
"expo-linking": "~7.0.4",
|
||||||
"expo-navigation-bar": "~4.0.8",
|
"expo-navigation-bar": "~4.0.8",
|
||||||
"expo-router": "~4.0.16",
|
"expo-router": "~4.0.16",
|
||||||
|
"expo-secure-store": "~14.0.1",
|
||||||
"expo-splash-screen": "~0.29.20",
|
"expo-splash-screen": "~0.29.20",
|
||||||
"expo-sqlite": "~15.1.2",
|
"expo-sqlite": "~15.1.2",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
@ -56,6 +58,7 @@
|
|||||||
"jest-expo": "~52.0.3",
|
"jest-expo": "~52.0.3",
|
||||||
"lucide-react-native": "^0.378.0",
|
"lucide-react-native": "^0.378.0",
|
||||||
"nativewind": "^4.1.23",
|
"nativewind": "^4.1.23",
|
||||||
|
"nostr-tools": "^2.10.4",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.7",
|
"react-native": "0.76.7",
|
||||||
@ -70,7 +73,7 @@
|
|||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.5.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
@ -3772,6 +3775,51 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/ciphers": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.7.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/secp256k1": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -3807,6 +3855,28 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nostr-dev-kit/ndk": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-B9NKdgn9CKNn0WHIFzj7SxeZhr+daT5im/ozj9Ey791MkaZiTB5XUCy5j9O15FDHTyFy0/gpCyq7LvJKIxCOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "^1.6.0",
|
||||||
|
"@noble/hashes": "^1.5.0",
|
||||||
|
"@noble/secp256k1": "^2.1.0",
|
||||||
|
"@scure/base": "^1.1.9",
|
||||||
|
"debug": "^4.3.6",
|
||||||
|
"light-bolt11-decoder": "^3.2.0",
|
||||||
|
"nostr-tools": "^2.7.1",
|
||||||
|
"tseep": "^1.2.2",
|
||||||
|
"typescript-lru-cache": "^2.0.0",
|
||||||
|
"utf8-buffer": "^1.0.0",
|
||||||
|
"websocket-polyfill": "^0.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@npmcli/fs": {
|
"node_modules/@npmcli/fs": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
|
||||||
@ -8688,6 +8758,108 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scure/base": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "~1.1.0",
|
||||||
|
"@noble/hashes": "~1.3.1",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@scure/base": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "~1.3.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39/node_modules/@scure/base": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@segment/loosely-validate-event": {
|
"node_modules/@segment/loosely-validate-event": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz",
|
||||||
@ -9952,6 +10124,19 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bufferutil": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@ -10840,6 +11025,19 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
||||||
@ -11362,6 +11560,46 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
|
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"esniff": "^2.0.1",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"ext": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@ -11444,6 +11682,21 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esniff": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.62",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esprima": {
|
"node_modules/esprima": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
@ -11497,6 +11750,16 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
@ -11916,6 +12179,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-secure-store": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-QUS+j4+UG4jRQalgnpmTvvrFnMVLqPiUZRzYPnG3+JrZ5kwVW2w6YS3WWerPoR7C6g3y/a2htRxRSylsDs+TaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-splash-screen": {
|
"node_modules/expo-splash-screen": {
|
||||||
"version": "0.29.22",
|
"version": "0.29.22",
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.22.tgz",
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.29.22.tgz",
|
||||||
@ -11975,6 +12247,15 @@
|
|||||||
"integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
|
"integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -13278,6 +13559,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-typedarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-wsl": {
|
"node_modules/is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
@ -14608,6 +14895,27 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/light-bolt11-decoder": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scure/base": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/light-bolt11-decoder/node_modules/@scure/base": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lighthouse-logger": {
|
"node_modules/lighthouse-logger": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz",
|
||||||
@ -15829,6 +16137,12 @@
|
|||||||
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
|
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/nice-try": {
|
"node_modules/nice-try": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
@ -15898,6 +16212,17 @@
|
|||||||
"node": ">= 6.13.0"
|
"node": ">= 6.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -15919,6 +16244,86 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nostr-tools": {
|
||||||
|
"version": "2.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
|
||||||
|
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "^0.5.1",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@scure/base": "1.1.1",
|
||||||
|
"@scure/bip32": "1.3.1",
|
||||||
|
"@scure/bip39": "1.2.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"nostr-wasm": "0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-tools/node_modules/@scure/base": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/npm-package-arg": {
|
"node_modules/npm-package-arg": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
|
||||||
@ -19315,18 +19720,36 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/tseep": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tstl": {
|
||||||
|
"version": "2.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
|
||||||
|
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/turbo-stream": {
|
"node_modules/turbo-stream": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
@ -19348,6 +19771,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray-to-buffer": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-typedarray": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
@ -19362,6 +19794,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript-lru-cache": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.40",
|
"version": "1.0.40",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
||||||
@ -19598,6 +20036,28 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utf-8-validate": {
|
||||||
|
"version": "5.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
|
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/utf8-buffer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util": {
|
"node_modules/util": {
|
||||||
"version": "0.12.5",
|
"version": "0.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
@ -19810,6 +20270,47 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/websocket": {
|
||||||
|
"version": "1.0.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
|
||||||
|
"integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"debug": "^2.2.0",
|
||||||
|
"es5-ext": "^0.10.63",
|
||||||
|
"typedarray-to-buffer": "^3.1.5",
|
||||||
|
"utf-8-validate": "^5.0.2",
|
||||||
|
"yaeti": "^0.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-polyfill": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tstl": "^2.0.7",
|
||||||
|
"websocket": "^1.0.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/whatwg-encoding": {
|
"node_modules/whatwg-encoding": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
@ -20064,6 +20565,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaeti": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/cli": "^0.22.16",
|
"@expo/cli": "^0.22.16",
|
||||||
|
"@nostr-dev-kit/ndk": "^2.12.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
"@react-navigation/material-top-tabs": "^7.1.0",
|
||||||
@ -62,6 +63,7 @@
|
|||||||
"expo-linking": "~7.0.4",
|
"expo-linking": "~7.0.4",
|
||||||
"expo-navigation-bar": "~4.0.8",
|
"expo-navigation-bar": "~4.0.8",
|
||||||
"expo-router": "~4.0.16",
|
"expo-router": "~4.0.16",
|
||||||
|
"expo-secure-store": "~14.0.1",
|
||||||
"expo-splash-screen": "~0.29.20",
|
"expo-splash-screen": "~0.29.20",
|
||||||
"expo-sqlite": "~15.1.2",
|
"expo-sqlite": "~15.1.2",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
@ -70,6 +72,7 @@
|
|||||||
"jest-expo": "~52.0.3",
|
"jest-expo": "~52.0.3",
|
||||||
"lucide-react-native": "^0.378.0",
|
"lucide-react-native": "^0.378.0",
|
||||||
"nativewind": "^4.1.23",
|
"nativewind": "^4.1.23",
|
||||||
|
"nostr-tools": "^2.10.4",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.7",
|
"react-native": "0.76.7",
|
||||||
@ -84,8 +87,7 @@
|
|||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zustand": "^4.4.7",
|
"zustand": "^4.5.6"
|
||||||
"expo-secure-store": "~14.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user