Skip to content

Commit

Permalink
[MOB-106] Cloud Sync for Mobile (#2549)
Browse files Browse the repository at this point in the history
* wip + working backfill

* Finished BackfillWaiting page + initial auth setup

Also, setting up the cloud sync page.

* mobile auth

* Import Cloud Library

Currently, you can import a cloud library, however it seems that the data, such as locations, is not transferring correctly.

* Working Mobile Cloud Sync

Cloud Sync works for Mobile, and the mobile app can sync files from a cloud library, and other clients can access the data from the phone's cloud library.

* Cloud Sync Done

* Formatting

* Fix new library button

* New device type passing to auth

* Improve design of cloud settings and import modal

* ui adjustments and code cleanup

* Update styling if there's only 1 instance

* code cleanup, design tweaks

* empty state & simple indicator animation

* lint

* loading indicator and cleanup

* Fix to Sync Subscription

* Update Cargo.lock

* Async logout for debug

* tweaks

* Update SettingsStack.tsx

* cleanups and cloud desktop design improvements

* more cleanups and ui improvements

* ts

* i18n

* Cloud Sync Docs

* styling

* Delete library-sync.mdx

Moving docs to a separate branch

---------

Co-authored-by: ameer2468 <[email protected]>
  • Loading branch information
Rocky43007 and ameer2468 committed Jun 18, 2024
1 parent e3202b3 commit 18235c6
Show file tree
Hide file tree
Showing 42 changed files with 1,277 additions and 75 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ If you encounter any issues, ensure that you are using the following versions of

- Rust version: **1.78**
- Node version: **18.18**
- Pnpm version: **9.0.6**
- Pnpm version: **9.1.1**

After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.

Expand Down
12 changes: 11 additions & 1 deletion apps/mobile/src/components/drawer/DrawerLibraryManager.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useDrawerStatus } from '@react-navigation/drawer';
import { useNavigation } from '@react-navigation/native';
import { MotiView } from 'moti';
import { CaretRight, Gear, Lock, Plus } from 'phosphor-react-native';
import { CaretRight, CloudArrowDown, Gear, Lock, Plus } from 'phosphor-react-native';
import { useEffect, useRef, useState } from 'react';
import { Alert, Pressable, Text, View } from 'react-native';
import { useClientContext } from '@sd/client';
Expand All @@ -12,6 +12,7 @@ import { AnimatedHeight } from '../animation/layout';
import { ModalRef } from '../layout/Modal';
import CreateLibraryModal from '../modal/CreateLibraryModal';
import { Divider } from '../primitive/Divider';
import ImportModalLibrary from '../modal/ImportLibraryModal';

const DrawerLibraryManager = () => {
const [dropdownClosed, setDropdownClosed] = useState(true);
Expand All @@ -27,6 +28,7 @@ const DrawerLibraryManager = () => {
const navigation = useNavigation();

const modalRef = useRef<ModalRef>(null);
const modalRef_import = useRef<ModalRef>(null);

return (
<View>
Expand Down Expand Up @@ -91,6 +93,14 @@ const DrawerLibraryManager = () => {
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
</Pressable>
<CreateLibraryModal ref={modalRef} />
<Pressable
style={tw`flex flex-row items-center px-1.5 py-[8px]`}
onPress={() => modalRef_import.current?.present()}
>
<CloudArrowDown size={18} weight="bold" color="white" style={tw`mr-2`} />
<Text style={tw`text-sm font-semibold text-white`}>Import Library</Text>
</Pressable>
<ImportModalLibrary ref={modalRef_import} />
{/* Manage Library */}
<Pressable
onPress={() => {
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/layout/ScreenContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const ScreenContainer = ({
}}
contentContainerStyle={twStyle('justify-between gap-10 py-6', style)}
style={twStyle(
'flex-1 bg-black',
'bg-black',
tabHeight && { marginBottom: bottomTabBarHeight }
)}
>
Expand Down
139 changes: 139 additions & 0 deletions apps/mobile/src/components/modal/ImportLibraryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { forwardRef } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import {
CloudLibrary,
useBridgeMutation,
useBridgeQuery,
useClientContext,
useRspcContext
} from '@sd/client';
import { Modal, ModalRef } from '~/components/layout/Modal';
import { Button } from '~/components/primitive/Button';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw } from '~/lib/tailwind';
import { RootStackParamList } from '~/navigation';
import { currentLibraryStore } from '~/utils/nav';

import Empty from '../layout/Empty';
import Fade from '../layout/Fade';

const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const modalRef = useForwardedRef(ref);

const { libraries } = useClientContext();

const cloudLibraries = useBridgeQuery(['cloud.library.list']);
const cloudLibrariesData = cloudLibraries.data?.filter(
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
);

return (
<Modal
ref={modalRef}
snapPoints={cloudLibrariesData?.length !== 0 ? ['30', '50'] : ['30']}
title="Join a Cloud Library"
showCloseButton
>
<View style={tw`relative flex-1`}>
{cloudLibraries.isLoading ? (
<View style={tw`mt-10 items-center justify-center`}>
<ActivityIndicator size="small" />
</View>
) : (
<Fade
width={20}
height="100%"
fadeSides="top-bottom"
orientation="vertical"
color="bg-app-modal"
>
<BottomSheetFlatList
data={cloudLibrariesData}
contentContainerStyle={tw`px-4 pb-6 pt-5`}
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
ListEmptyComponent={
<Empty
icon="Drive"
style={tw`mt-2 border-0`}
iconSize={46}
description="You don't have any cloud libraries"
/>
}
keyExtractor={(item) => item.uuid}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<CloudLibraryCard
data={item}
navigation={navigation}
modalRef={modalRef}
/>
)}
/>
</Fade>
)}
</View>
</Modal>
);
});

interface Props {
data: CloudLibrary;
modalRef: React.RefObject<ModalRef>;
navigation: NavigationProp<RootStackParamList>;
}

const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => {
const rspc = useRspcContext().queryClient;
const joinLibrary = useBridgeMutation(['cloud.library.join']);
return (
<View
key={data.uuid}
style={tw`flex flex-row items-center justify-between gap-2 rounded-md border border-app-box bg-app p-2`}
>
<Text numberOfLines={1} style={tw`max-w-[80%] text-sm font-bold text-ink`}>
{data.name}
</Text>
<Button
size="sm"
variant="accent"
disabled={joinLibrary.isLoading}
onPress={async () => {
const library = await joinLibrary.mutateAsync(data.uuid);

rspc.setQueryData(['library.list'], (libraries: any) => {
// The invalidation system beat us to it
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
return libraries;

return [...(libraries || []), library];
});

currentLibraryStore.id = library.uuid;

navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'OverviewStack',
params: {
screen: 'Overview'
}
}
});

modalRef.current?.dismiss();
}}
>
<Text style={tw`text-sm font-medium text-white`}>
{joinLibrary.isLoading && joinLibrary.variables === data.uuid
? 'Joining...'
: 'Join'}
</Text>
</Button>
</View>
);
};

export default ImportModalLibrary;
26 changes: 26 additions & 0 deletions apps/mobile/src/navigation/BackfillWaitingStack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack';
import React from 'react';
import BackfillWaiting from '~/screens/BackfillWaiting';

const Stack = createNativeStackNavigator<BackfillWaitingStackParamList>();

export default function BackfillWaitingStack() {
return (
<Stack.Navigator initialRouteName="BackfillWaiting">
<Stack.Screen
name="BackfillWaiting"
component={BackfillWaiting}
options={{
headerShown: false
}}
/>
</Stack.Navigator>
);
}

export type BackfillWaitingStackParamList = {
BackfillWaiting: undefined;
};

export type BackfillWaitingStackScreenProps<Screen extends keyof BackfillWaitingStackParamList> =
NativeStackScreenProps<BackfillWaitingStackParamList, Screen>;
7 changes: 7 additions & 0 deletions apps/mobile/src/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import NotFoundScreen from '~/screens/NotFound';

import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator';
import SearchStack, { SearchStackParamList } from './SearchStack';
import BackfillWaitingStack, { BackfillWaitingStackParamList } from './BackfillWaitingStack';

const Stack = createNativeStackNavigator<RootStackParamList>();
// This is the main navigator we nest everything under.
Expand All @@ -20,6 +21,11 @@ export default function RootNavigator() {
component={SearchStack}
options={{ headerShown: false }}
/>
<Stack.Screen
name="BackfillWaitingStack"
component={BackfillWaitingStack}
options={{ headerShown: false }}
/>
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
</Stack.Navigator>
);
Expand All @@ -28,6 +34,7 @@ export default function RootNavigator() {
export type RootStackParamList = {
Root: NavigatorScreenParams<DrawerNavParamList>;
SearchStack: NavigatorScreenParams<SearchStackParamList>;
BackfillWaitingStack: NavigatorScreenParams<BackfillWaitingStackParamList>;
NotFound: undefined;
};

Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/src/navigation/tabs/SettingsStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import PrivacySettingsScreen from '~/screens/settings/client/PrivacySettings';
import AboutScreen from '~/screens/settings/info/About';
import DebugScreen from '~/screens/settings/info/Debug';
import SupportScreen from '~/screens/settings/info/Support';
import CloudSettings from '~/screens/settings/library/CloudSettings/CloudSettings';
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
import LocationSettingsScreen from '~/screens/settings/library/LocationSettings';
import NodesSettingsScreen from '~/screens/settings/library/NodesSettings';
import SyncSettingsScreen from '~/screens/settings/library/SyncSettings';
import TagsSettingsScreen from '~/screens/settings/library/TagsSettings';
import SettingsScreen from '~/screens/settings/Settings';

Expand Down Expand Up @@ -87,6 +89,16 @@ export default function SettingsStack() {
component={TagsSettingsScreen}
options={{ header: () => <Header navBack title="Tags" /> }}
/>
<Stack.Screen
name="SyncSettings"
component={SyncSettingsScreen}
options={{ header: () => <Header navBack title="Sync" /> }}
/>
<Stack.Screen
name="CloudSettings"
component={CloudSettings}
options={{ header: () => <Header navBack title="Cloud" /> }}
/>
{/* <Stack.Screen
name="KeysSettings"
component={KeysSettingsScreen}
Expand Down Expand Up @@ -131,6 +143,8 @@ export type SettingsStackParamList = {
NodesSettings: undefined;
TagsSettings: undefined;
KeysSettings: undefined;
SyncSettings: undefined;
CloudSettings: undefined;
// Info
About: undefined;
Support: undefined;
Expand Down
87 changes: 87 additions & 0 deletions apps/mobile/src/screens/BackfillWaiting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useNavigation } from '@react-navigation/native';
import { AppLogo } from '@sd/assets/images';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Image } from 'expo-image';
import React, { useEffect } from 'react';
import { Dimensions, Text, View } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming
} from 'react-native-reanimated';
import { Circle, Defs, RadialGradient, Stop, Svg } from 'react-native-svg';
import { tw, twStyle } from '~/lib/tailwind';

const { width } = Dimensions.get('window');

const BackfillWaiting = () => {
const animation = useSharedValue(0);
const navigation = useNavigation();

useEffect(() => {
animation.value = withRepeat(
withTiming(1, { duration: 5000, easing: Easing.inOut(Easing.ease) }),
-1,
true
);
}, [animation]);

const animatedStyle = useAnimatedStyle(() => {
return {
opacity: animation.value
};
});

const enableSync = useLibraryMutation(['sync.backfill'], {
onSuccess: () => {
syncEnabled.refetch();
navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'SettingsStack',
params: {
screen: 'SyncSettings'
}
}
});
}
});

const syncEnabled = useLibraryQuery(['sync.enabled']);

useEffect(() => {
(async () => {
await enableSync.mutateAsync(null);
})();
}, []);

Check warning on line 58 in apps/mobile/src/screens/BackfillWaiting.tsx

View workflow job for this annotation

GitHub Actions / ESLint

React Hook useEffect has a missing dependency: 'enableSync'. Either include it or remove the dependency array

return (
<View style={tw`flex-1 items-center justify-center bg-black`}>
<Animated.View style={[twStyle(`absolute items-center justify-center`, {
width: width * 2,
height: width * 2,
borderRadius: (width * 0.8) / 2,
}), animatedStyle]}>
<Svg height="100%" width="100%" viewBox="0 0 100 100">
<Defs>
<RadialGradient id="grad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<Stop offset="0%" stopColor="#4B0082" stopOpacity="1" />
<Stop offset="100%" stopColor="#000000" stopOpacity="0" />
</RadialGradient>
</Defs>
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
</Svg>
</Animated.View>
<Image source={AppLogo} style={tw`mb-4 h-[100px] w-[100px]`} />
<Text style={tw`mx-10 mb-4 text-center text-md leading-6 text-ink`}>
Library is being backfilled right now for Sync!
<Text style={tw`font-bold`}> Please hold </Text>
while this process takes place.
</Text>
</View>
);
};

export default BackfillWaiting;
Loading

0 comments on commit 18235c6

Please sign in to comment.