Init libraries with rspc + fix feedback stuff

This commit is contained in:
Utku Bakir
2022-09-01 13:45:25 +03:00
parent 297b9ccb7c
commit 83781be27a
17 changed files with 233 additions and 81 deletions

View File

@@ -29,6 +29,7 @@
"expo-linking": "~3.2.2",
"expo-splash-screen": "~0.16.2",
"expo-status-bar": "~1.4.0",
"immer": "^9.0.15",
"intl": "^1.2.5",
"moti": "^0.18.0",
"phosphor-react-native": "^1.1.2",

View File

Binary file not shown.

View File

@@ -8,12 +8,19 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useDeviceContext } from 'twrnc';
import { GlobalModals } from './components/modals/GlobalModals';
import { ReactNativeTransport, queryClient, rspc, useInvalidateQuery } from './hooks/rspc';
import {
ReactNativeTransport,
queryClient,
rspc,
useBridgeQuery,
useInvalidateQuery
} from './hooks/rspc';
import useCachedResources from './hooks/useCachedResources';
import { getItemFromStorage } from './lib/storage';
import tw from './lib/tailwind';
import RootNavigator from './navigation';
import OnboardingNavigator from './navigation/OnboardingNavigator';
import { useLibraryStore } from './stores/useLibraryStore';
import { useOnboardingStore } from './stores/useOnboardingStore';
import type { Operations } from './types/bindings';
@@ -29,7 +36,7 @@ const NavigatorTheme: Theme = {
}
};
export default function App() {
function AppContainer() {
// Enables dark mode, and screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
@@ -37,38 +44,62 @@ export default function App() {
const { showOnboarding, hideOnboarding } = useOnboardingStore();
const { data: libraries } = useBridgeQuery(['library.get']);
const { switchLibrary, _hasHydrated } = useLibraryStore();
// Runs when the app is launched
useEffect(() => {
getItemFromStorage('@onboarding').then((value) => {
value && hideOnboarding();
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function appLaunch() {
// Check if the user went through onboarding
const didOnboarding = await getItemFromStorage('@onboarding');
// If user did do onboarding, that means they've already have a library
if (!isLoadingComplete) {
// Temporarly set the first library to be the current library
if (libraries && libraries.length > 0) {
switchLibrary(libraries[0].uuid);
}
if (didOnboarding) {
hideOnboarding();
}
}
appLaunch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [libraries]);
// Might need to move _hasHydrated to useCacheResources hook.
if (!isLoadingComplete && !_hasHydrated) {
return null;
} else {
return (
<rspc.Provider client={client} queryClient={queryClient}>
<>
<InvalidateQuery />
<SafeAreaProvider style={tw`flex-1 bg-black`}>
<GestureHandlerRootView style={tw`flex-1`}>
<BottomSheetModalProvider>
<StatusBar style="light" />
<NavigationContainer theme={NavigatorTheme}>
{showOnboarding ? <OnboardingNavigator /> : <RootNavigator />}
</NavigationContainer>
<GlobalModals />
</BottomSheetModalProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
</>
</rspc.Provider>
<SafeAreaProvider style={tw`flex-1 bg-black`}>
<GestureHandlerRootView style={tw`flex-1`}>
<BottomSheetModalProvider>
<StatusBar style="light" />
<NavigationContainer theme={NavigatorTheme}>
{showOnboarding ? <OnboardingNavigator /> : <RootNavigator />}
</NavigationContainer>
<GlobalModals />
</BottomSheetModalProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
);
}
}
export default function App() {
return (
<rspc.Provider client={client} queryClient={queryClient}>
<>
<InvalidateQuery />
<AppContainer />
</>
</rspc.Provider>
);
}
function InvalidateQuery() {
useInvalidateQuery();
return null;

View File

@@ -1,14 +1,14 @@
import Layout from '@app/constants/Layout';
import tw from '@app/lib/tailwind';
import { useCurrentLibrary, useLibraryStore } from '@app/stores/useLibraryStore';
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types';
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';
import React from 'react';
import React, { useEffect } from 'react';
import { ColorValue, Platform, Pressable, Text, View } from 'react-native';
import { CogIcon } from 'react-native-heroicons/solid';
import CollapsibleView from '../layout/CollapsibleView';
import { Tooltip } from '../tooltip/Tooltip';
import DrawerLocationItem from './DrawerLocationItem';
import DrawerLogo from './DrawerLogo';
import DrawerTagItem from './DrawerTagItem';
@@ -57,12 +57,22 @@ const getActiveRouteState = function (state: any) {
const DrawerContent = ({ navigation, state }: DrawerContentComponentProps) => {
const stackName = getFocusedRouteNameFromRoute(getActiveRouteState(state)) ?? 'OverviewStack';
// initialize libraries
const { init: initLibraries, switchLibrary } = useLibraryStore();
const { currentLibrary, libraries, currentLibraryUuid } = useCurrentLibrary();
useEffect(() => {
if (libraries && !currentLibraryUuid) initLibraries(libraries);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [libraries, currentLibraryUuid]);
return (
<DrawerContentScrollView style={tw`flex-1 px-4 py-2`} scrollEnabled={false}>
<View style={tw.style('justify-between', { height: drawerHeight })}>
<View>
<DrawerLogo />
<Text style={tw`my-4 text-white text-xs`}>TODO: Library Selection</Text>
<Text style={tw`text-white`}>{libraries[0].config.name}</Text>
{/* Locations */}
<CollapsibleView
title="Locations"
@@ -108,11 +118,9 @@ const DrawerContent = ({ navigation, state }: DrawerContentComponentProps) => {
</CollapsibleView>
</View>
{/* Settings */}
<Tooltip label="Settings">
<Pressable onPress={() => navigation.navigate('Settings')}>
<CogIcon color="white" size={24} />
</Pressable>
</Tooltip>
<Pressable onPress={() => navigation.navigate('Settings')}>
<CogIcon color="white" size={24} />
</Pressable>
</View>
</DrawerContentScrollView>
);

View File

@@ -0,0 +1,32 @@
import tw from '@app/lib/tailwind';
import { VariantProps, cva } from 'class-variance-authority';
import React from 'react';
import { TextInput as RNTextInput, TextInputProps as RNTextInputProps } from 'react-native';
const input = cva(['text-sm rounded-md border shadow-sm'], {
variants: {
variant: {
default: 'bg-gray-550 border-gray-500 text-white'
},
size: {
default: ['py-2', 'px-3']
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
type InputProps = VariantProps<typeof input> & RNTextInputProps;
export const TextInput: React.FC<InputProps> = ({ variant, ...props }) => {
const { style, ...otherProps } = props;
return (
<RNTextInput
placeholderTextColor={tw.color('gray-300')}
style={tw.style(input({ variant }), style as string)}
{...otherProps}
/>
);
};

View File

@@ -13,19 +13,15 @@ export default function DrawerNavigator() {
return (
<Drawer.Navigator
initialRouteName="Home"
screenOptions={({ route }) => {
return {
headerShown: false,
drawerStyle: {
backgroundColor: '#08090D',
width: '75%'
},
overlayColor: 'transparent',
drawerType: 'slide'
// swipeEnabled: false
// drawerHideStatusBarOnOpen: true,
// drawerStatusBarAnimation: 'slide'
};
screenOptions={{
headerShown: false,
drawerStyle: {
backgroundColor: '#08090D',
width: '75%'
},
overlayColor: 'transparent',
drawerType: 'slide',
swipeEdgeWidth: 50
}}
drawerContent={(props) => <DrawerContent {...(props as any)} />}
>

View File

@@ -1,3 +1,4 @@
import CreateLibraryScreen from '@app/screens/onboarding/CreateLibrary';
import OnboardingScreen from '@app/screens/onboarding/Onboarding';
import { StackScreenProps, createStackNavigator } from '@react-navigation/stack';
@@ -7,12 +8,14 @@ export default function OnboardingNavigator() {
return (
<OnboardingStack.Navigator screenOptions={{ headerShown: false }}>
<OnboardingStack.Screen name="Onboarding" component={OnboardingScreen} />
<OnboardingStack.Screen name="CreateLibrary" component={CreateLibraryScreen} />
</OnboardingStack.Navigator>
);
}
export type OnboardingStackParamList = {
Onboarding: undefined;
CreateLibrary: undefined;
};
export type OnboardingStackScreenProps<Screen extends keyof OnboardingStackParamList> =

View File

@@ -15,7 +15,11 @@ export default function RootNavigator() {
<Stack.Navigator initialRouteName="Root">
<Stack.Screen name="Root" component={DrawerNavigator} options={{ headerShown: false }} />
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
<Stack.Screen name="Search" component={SearchScreen} options={{ headerShown: false }} />
<Stack.Screen
name="Search"
component={SearchScreen}
options={{ headerShown: false, animationEnabled: false }}
/>
{/* Modals */}
<Stack.Group
screenOptions={{

View File

@@ -1,11 +1,7 @@
import Header from '@app/components/header/Header';
import BrowseScreen from '@app/screens/Browse';
import { CompositeScreenProps } from '@react-navigation/native';
import {
Header,
StackScreenProps,
TransitionPresets,
createStackNavigator
} from '@react-navigation/stack';
import { StackScreenProps, createStackNavigator } from '@react-navigation/stack';
import { SharedScreens, SharedScreensParamList } from '../SharedScreens';
import { TabScreenProps } from '../TabNavigator';
@@ -18,8 +14,7 @@ export default function BrowseStack() {
initialRouteName="Browse"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
headerTintColor: '#fff',
...TransitionPresets.ModalFadeTransition
headerTintColor: '#fff'
}}
>
<Stack.Screen name="Browse" component={BrowseScreen} options={{ header: Header }} />

View File

@@ -14,8 +14,7 @@ export default function OverviewStack() {
initialRouteName="Overview"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
headerTintColor: '#fff',
...TransitionPresets.ModalFadeTransition
headerTintColor: '#fff'
}}
>
<Stack.Screen name="Overview" component={OverviewScreen} options={{ header: Header }} />

View File

@@ -14,8 +14,7 @@ export default function PhotosStack() {
initialRouteName="Photos"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
headerTintColor: '#fff',
...TransitionPresets.ModalFadeTransition
headerTintColor: '#fff'
}}
>
<Stack.Screen name="Photos" component={PhotosScreen} options={{ header: Header }} />

View File

@@ -14,8 +14,7 @@ export default function SpacesStack() {
initialRouteName="Spaces"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
headerTintColor: '#fff',
...TransitionPresets.ModalFadeTransition
headerTintColor: '#fff'
}}
>
<Stack.Screen name="Spaces" component={SpacesScreen} options={{ header: Header }} />

View File

@@ -1,7 +1,9 @@
import Device from '@app/components/device/Device';
import Dialog from '@app/components/layout/Dialog';
import VirtualizedListWrapper from '@app/components/layout/VirtualizedListWrapper';
import { TextInput } from '@app/components/primitive/Input';
import OverviewStats from '@app/containers/OverviewStats';
import { useLibraryQuery } from '@app/hooks/rspc';
import tw from '@app/lib/tailwind';
import { OverviewStackScreenProps } from '@app/navigation/tabs/OverviewStack';
import React from 'react';
@@ -61,11 +63,11 @@ export default function OverviewScreen({ navigation }: OverviewStackScreenProps<
<Text>Dialog</Text>
</View>
}
/>
>
<TextInput placeholder="My Cool Library" />
</Dialog>
{/* Stats */}
<OverviewStats stats={placeholderOverviewStats} />
{/* Spacing */}
<View style={tw`mt-4`} />
{/* Devices */}
<FlatList
data={placeholderDevices}

View File

@@ -11,12 +11,14 @@ const SearchScreen = ({ navigation }: RootStackScreenProps<'Search'>) => {
const [loading, setLoading] = useState(false);
// TODO: Animations!
return (
<View style={tw.style('flex-1', { marginTop: top + 10 })}>
{/* Header */}
<View style={tw`flex flex-row items-center mx-4`}>
{/* Search Input */}
<View style={tw`flex-1 bg-gray-550 rounded-md h-9 mr-3`}>
<View style={tw`flex-1 bg-gray-550 rounded-md h-10 mr-3`}>
<View style={tw`flex flex-row h-full items-center px-3`}>
<View style={tw`mr-3`}>
{loading ? (

View File

@@ -0,0 +1,31 @@
import { AnimatedButton } from '@app/components/primitive/Button';
import { setItemToStorage } from '@app/lib/storage';
import tw from '@app/lib/tailwind';
import { OnboardingStackScreenProps } from '@app/navigation/OnboardingNavigator';
import { useOnboardingStore } from '@app/stores/useOnboardingStore';
import React from 'react';
import { Text, View } from 'react-native';
const CreateLibraryScreen = ({ navigation }: OnboardingStackScreenProps<'CreateLibrary'>) => {
const { hideOnboarding } = useOnboardingStore();
function onButtonPress() {
setItemToStorage('@onboarding', '1');
// TODO: Add a loading indicator to button as this takes a second or so.
hideOnboarding();
}
return (
<View style={tw`flex-1 items-center justify-center bg-black p-4`}>
<Text style={tw`text-gray-450 text-center px-6 my-8 text-base leading-relaxed`}>
Onboarding screen for users to create their first library
</Text>
<AnimatedButton variant="primary" onPress={onButtonPress}>
<Text style={tw`text-white text-center px-6 py-2 text-base font-medium`}>
Create Library
</Text>
</AnimatedButton>
</View>
);
};
export default CreateLibraryScreen;

View File

@@ -1,21 +1,11 @@
import { FadeInUpAnimation, LogoAnimation } from '@app/components/animation/layout';
import { AnimatedButton } from '@app/components/primitive/Button';
import { setItemToStorage } from '@app/lib/storage';
import tw from '@app/lib/tailwind';
import { OnboardingStackScreenProps } from '@app/navigation/OnboardingNavigator';
import { useOnboardingStore } from '@app/stores/useOnboardingStore';
import React from 'react';
import { Image, Text, View } from 'react-native';
const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding'>) => {
const { hideOnboarding } = useOnboardingStore();
function onButtonPress() {
setItemToStorage('@onboarding', '1');
// TODO: Add a loading indicator to button as this takes a second or so.
hideOnboarding();
}
return (
<View style={tw`flex-1 items-center justify-around bg-black p-4 z-10`}>
{/* Logo */}
@@ -25,7 +15,7 @@ const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding
</View>
</LogoAnimation>
{/* Text */}
<View style={tw``}>
<View>
<FadeInUpAnimation delay={500}>
<Text style={tw`text-white text-center text-5xl font-black leading-tight`}>
A file explorer from the future.
@@ -40,7 +30,7 @@ const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding
</View>
{/* Get Started Button */}
<FadeInUpAnimation delay={1200}>
<AnimatedButton variant="primary" onPress={onButtonPress}>
<AnimatedButton variant="primary" onPress={() => navigation.navigate('CreateLibrary')}>
<Text style={tw`text-white text-center px-6 py-2 text-base font-medium`}>
Get Started
</Text>

View File

@@ -1,13 +1,73 @@
import { useBridgeQuery } from '@app/hooks/rspc';
import { LibraryConfigWrapped } from '@app/types/bindings';
import AsyncStorage from '@react-native-async-storage/async-storage';
import produce from 'immer';
import { useMemo } from 'react';
import create from 'zustand';
import { persist } from 'zustand/middleware';
interface LibraryStore {
_hasHydrated: boolean;
setHasHydrated: (hasHydrated: boolean) => void;
currentLibraryUuid: string | null;
switchLibrary: (id: string) => void;
init: (libraries: LibraryConfigWrapped[]) => Promise<void>;
}
export const useLibraryStore = create<LibraryStore>()((set) => ({
currentLibraryUuid: null,
switchLibrary: (uuid) => {
set((state) => ({ currentLibraryUuid: uuid }));
}
}));
export const useLibraryStore = create<LibraryStore>()(
persist(
(set) => ({
_hasHydrated: false,
setHasHydrated: (state) => {
set({ _hasHydrated: state });
},
currentLibraryUuid: null,
switchLibrary: (uuid) => {
set((state) =>
produce(state, (draft) => {
draft.currentLibraryUuid = uuid;
})
);
// reset other stores
},
init: async (libraries) => {
set((state) =>
produce(state, (draft) => {
// use first library default if none set
if (!state.currentLibraryUuid) {
draft.currentLibraryUuid = libraries[0].uuid;
}
})
);
}
}),
{
name: 'sd-library-store',
getStorage: () => AsyncStorage,
// Since storage is async, app needs to stay in loading state until hydrated!
onRehydrateStorage: () => (state) => {
state.setHasHydrated(true);
}
}
)
);
// this must be used at least once in the app to correct the initial state
// is memorized and can be used safely in any component
export const useCurrentLibrary = () => {
const { currentLibraryUuid, switchLibrary } = useLibraryStore();
const { data: libraries } = useBridgeQuery(['library.get']);
// memorize library to avoid re-running find function
const currentLibrary = useMemo(() => {
const current = libraries?.find((l) => l.uuid === currentLibraryUuid);
// switch to first library if none set
if (Array.isArray(libraries) && !current && libraries[0]?.uuid) {
switchLibrary(libraries[0]?.uuid);
}
return current;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [libraries, currentLibraryUuid]);
return { currentLibrary, libraries, currentLibraryUuid };
};