Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { DeactivatedAccountScreen } from '../screens/DeactivatedAccountScreen';
import DebugScreen from '../screens/DebugScreen';
import { TrashScreen } from '../screens/common/TrashScreen';
import { DrivePreviewScreen } from '../screens/drive/DrivePreviewScreen';
import AndroidShareScreen from '../shareExtension/AndroidShareScreen';
import ShareExtensionView from '../shareExtension/ShareExtensionView.android';
import { useAndroidShareIntent } from '../shareExtension/useAndroidShareIntent';

const Stack = createNativeStackNavigator<RootStackParamList>();
Expand Down Expand Up @@ -90,7 +90,7 @@ function AppNavigator({ navigationContainerRef }: Readonly<Props>): JSX.Element
{Platform.OS === 'android' && (
<Stack.Screen
name="AndroidShare"
component={AndroidShareScreen}
component={ShareExtensionView}
options={{ animation: 'slide_from_bottom', gestureEnabled: false }}
/>
)}
Expand Down
174 changes: 174 additions & 0 deletions src/shareExtension/hooks/useFolderNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { DriveListViewMode } from '@internxt-mobile/types/drive/ui';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { shareDriveService } from '../services/shareDriveService';
import { ShareFileItem, ShareFolderItem } from '../types';

interface FolderNavEntry {
uuid: string;
name: string;
}

interface UseFolderNavigationResult {
currentFolder: FolderNavEntry;
folders: ShareFolderItem[];
files: ShareFileItem[];
loading: boolean;
loadingMore: boolean;
loadMore: () => void;
searchQuery: string;
setSearchQuery: (value: string) => void;
viewMode: DriveListViewMode;
setViewMode: (viewMode: DriveListViewMode) => void;
breadcrumb: FolderNavEntry[];
navigate: (uuid: string, name: string) => void;
goBack: () => void;
refresh: () => Promise<void>;
createFolder: (name: string) => Promise<void>;
}

const filterByName = <T extends { plainName: string }>(items: T[], query: string): T[] => {
if (!query) return items;
const queryLowerCase = query.toLowerCase();
return items.filter((item) => item.plainName.toLowerCase().includes(queryLowerCase));
};

export const useFolderNavigation = (rootFolderUuid: string, rootFolderName = 'Drive'): UseFolderNavigationResult => {
const [folderStack, setFolderStack] = useState<FolderNavEntry[]>([{ uuid: rootFolderUuid, name: rootFolderName }]);
const [allFolders, setAllFolders] = useState<ShareFolderItem[]>([]);
const [allFiles, setAllFiles] = useState<ShareFileItem[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<DriveListViewMode>(DriveListViewMode.List);

const folderOffsetRef = useRef(0);
const fileOffsetRef = useRef(0);
const foldersExhaustedRef = useRef(false);
const filesExhaustedRef = useRef(false);
const isLoadingMoreRef = useRef(false);
const latestUuidRef = useRef<string>(rootFolderUuid);
const loadSequentialRef = useRef(0);

const currentFolder = folderStack[folderStack.length - 1];

const loadFolder = useCallback(async (folderUuid: string) => {
const seq = ++loadSequentialRef.current;
latestUuidRef.current = folderUuid;
isLoadingMoreRef.current = false;
folderOffsetRef.current = 0;
fileOffsetRef.current = 0;
foldersExhaustedRef.current = false;
filesExhaustedRef.current = false;
setLoading(true);
setLoadingMore(false);
setAllFolders([]);
setAllFiles([]);

try {
const foldersPage = await shareDriveService.getFolderFolders(folderUuid, 0);
if (loadSequentialRef.current !== seq) return;

setAllFolders(foldersPage.items);
folderOffsetRef.current = foldersPage.items.length;

if (!foldersPage.hasMore) {
foldersExhaustedRef.current = true;
const filesPage = await shareDriveService.getFolderFiles(folderUuid, 0);
if (loadSequentialRef.current !== seq) return;

setAllFiles(filesPage.items);
fileOffsetRef.current = filesPage.items.length;
if (!filesPage.hasMore) filesExhaustedRef.current = true;
}
} finally {
if (loadSequentialRef.current === seq) setLoading(false);
}
}, []);

useEffect(() => {
loadFolder(currentFolder.uuid);
}, [currentFolder.uuid, loadFolder]);

const loadMore = useCallback(async () => {

Check failure on line 92 in src/shareExtension/hooks/useFolderNavigation.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-mobile&issues=AZzDZHZK63NKbvqNGH57&open=AZzDZHZK63NKbvqNGH57&pullRequest=381
if (loading) return;
if (isLoadingMoreRef.current) return;
if (searchQuery) return;
if (foldersExhaustedRef.current && filesExhaustedRef.current) return;

isLoadingMoreRef.current = true;
setLoadingMore(true);
const uuid = latestUuidRef.current;
const seq = loadSequentialRef.current;

try {
if (!foldersExhaustedRef.current) {

Check warning on line 104 in src/shareExtension/hooks/useFolderNavigation.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-mobile&issues=AZzDZHZK63NKbvqNGH58&open=AZzDZHZK63NKbvqNGH58&pullRequest=381
const foldersPage = await shareDriveService.getFolderFolders(uuid, folderOffsetRef.current);
if (loadSequentialRef.current !== seq) return;

setAllFolders((prev) => [...prev, ...foldersPage.items]);
folderOffsetRef.current += foldersPage.items.length;

if (!foldersPage.hasMore) {
foldersExhaustedRef.current = true;
const filesPage = await shareDriveService.getFolderFiles(uuid, 0);
if (loadSequentialRef.current !== seq) return;

setAllFiles((prev) => [...prev, ...filesPage.items]);
fileOffsetRef.current = filesPage.items.length;
if (!filesPage.hasMore) filesExhaustedRef.current = true;
}
} else {
const filesPage = await shareDriveService.getFolderFiles(uuid, fileOffsetRef.current);
if (loadSequentialRef.current !== seq) return;

setAllFiles((prev) => [...prev, ...filesPage.items]);
fileOffsetRef.current += filesPage.items.length;
if (!filesPage.hasMore) filesExhaustedRef.current = true;
}
} finally {
if (loadSequentialRef.current === seq) setLoadingMore(false);
isLoadingMoreRef.current = false;
}
}, [loading, searchQuery]);

const navigateToFolder = useCallback((uuid: string, name: string) => {
setSearchQuery('');
setFolderStack((prev) => [...prev, { uuid, name }]);
}, []);

const goBack = useCallback(() => {
setSearchQuery('');
setFolderStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
}, []);

const refresh = useCallback(() => loadFolder(currentFolder.uuid), [currentFolder.uuid, loadFolder]);

const createFolder = useCallback(
async (name: string) => {
await shareDriveService.createFolder(currentFolder.uuid, name);
await loadFolder(currentFolder.uuid);
},
[currentFolder.uuid, loadFolder],
);

const folders = useMemo(() => filterByName(allFolders, searchQuery), [allFolders, searchQuery]);
const files = useMemo(() => filterByName(allFiles, searchQuery), [allFiles, searchQuery]);

return {
currentFolder,
folders,
files,
loading,
loadingMore,
loadMore,
searchQuery,
setSearchQuery,
viewMode,
setViewMode,
breadcrumb: folderStack,
navigate: navigateToFolder,
goBack,
refresh,
createFolder,
};
};
39 changes: 32 additions & 7 deletions src/shareExtension/hooks/useShareAuth.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@ import { AsyncStorageKey } from '../../types';

export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';

export const useShareAuth = (): AuthStatus => {
const [status, setStatus] = useState<AuthStatus>('loading');
export type ShareAuthData = {
status: AuthStatus;
photosToken: string | null;
mnemonic: string | null;
rootFolderUuid: string | null;
};

export const useShareAuth = (): ShareAuthData => {
const [data, setData] = useState<ShareAuthData>({
status: 'loading',
photosToken: null,
mnemonic: null,
rootFolderUuid: null,
});

useEffect(() => {
asyncStorageService
.getItem(AsyncStorageKey.PhotosToken)
.then((token) => setStatus(token ? 'authenticated' : 'unauthenticated'))
.catch(() => setStatus('unauthenticated'));
Promise.all([asyncStorageService.getItem(AsyncStorageKey.PhotosToken), asyncStorageService.getUser()])
.then(([photosToken, user]) => {
setData({
status: photosToken ? 'authenticated' : 'unauthenticated',
photosToken,
mnemonic: user?.mnemonic ?? null,
rootFolderUuid: user?.rootFolderId ?? null,
});
})
.catch(() =>
setData({
status: 'unauthenticated',
photosToken: null,
mnemonic: null,
rootFolderUuid: null,
}),
);
}, []);

return status;
return data;
};
15 changes: 15 additions & 0 deletions src/shareExtension/hooks/useShareExtension.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMemo } from 'react';
import { SharedFile } from '../types';
import { readSize } from '../utils';
import { useShareAuth } from './useShareAuth.android';

export const useShareExtension = (rawFiles: SharedFile[]) => {
const { status, rootFolderUuid } = useShareAuth();

const sharedFiles = useMemo(
() => rawFiles.map((file) => ({ ...file, size: file.size ?? readSize(file.uri) })),
[rawFiles],
);

return { status, rootFolderUuid, sharedFiles };
};
42 changes: 42 additions & 0 deletions src/shareExtension/hooks/useShareExtension.ios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect, useMemo, useState } from 'react';
import { SdkManager } from '../../services/common/sdk/SdkManager';
import { SharedFile } from '../types';
import { readSize } from '../utils';

interface ShareExtensionInput {
photosToken?: string;
files?: string[];
images?: string[];
videos?: string[];
}

export const useShareExtension = ({ photosToken, files, images, videos }: ShareExtensionInput) => {
const [sdkReady, setSdkReady] = useState(false);

useEffect(() => {
if (!photosToken) return;
SdkManager.init({ token: photosToken, newToken: photosToken });
setSdkReady(true);
}, [photosToken]);

const sharedFiles = useMemo<SharedFile[]>(
() => [
...(files ?? []).map((uri) => ({ uri, mimeType: null, fileName: uri.split('/').pop() ?? null, size: readSize(uri) })),
...(images ?? []).map((uri) => ({
uri,
mimeType: 'image/jpeg',
fileName: uri.split('/').pop() ?? null,
size: readSize(uri),
})),
...(videos ?? []).map((uri) => ({
uri,
mimeType: 'video/mp4',
fileName: uri.split('/').pop() ?? null,
size: readSize(uri),
})),
],
[files, images, videos],
);

return { sdkReady, sharedFiles };
};
59 changes: 59 additions & 0 deletions src/shareExtension/services/shareDriveService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { SdkManager } from '../../services/common/sdk/SdkManager';
import { ShareFileItem, ShareFolderItem } from '../types';

const PAGE_SIZE = 50;

const mapFolder = (folder: { uuid: string; plainName: string; updatedAt: string }): ShareFolderItem => ({
uuid: folder.uuid,
plainName: folder.plainName,
updatedAt: folder.updatedAt,
});

const mapFile = (file: {
uuid: string;
plainName: string;
size: string;
type?: string | null;
updatedAt: string;
}): ShareFileItem => ({
uuid: file.uuid,
plainName: file.plainName,
size: file.size,
type: file.type ?? '',
updatedAt: file.updatedAt,
});

const getFolderFolders = async (
folderUuid: string,
offset: number,
): Promise<{ items: ShareFolderItem[]; hasMore: boolean }> => {
const sdk = SdkManager.getInstance();
const [promise] = sdk.storageV2.getFolderFoldersByUuid(folderUuid, offset, PAGE_SIZE, 'plainName', 'ASC');
const result = await promise;
return {
items: result.folders.map(mapFolder),
hasMore: result.folders.length >= PAGE_SIZE,
};
};

const getFolderFiles = async (
folderUuid: string,
offset: number,
): Promise<{ items: ShareFileItem[]; hasMore: boolean }> => {
const sdk = SdkManager.getInstance();
const [promise] = sdk.storageV2.getFolderFilesByUuid(folderUuid, offset, PAGE_SIZE, 'plainName', 'ASC');
const result = await promise;
return {
items: result.files.map(mapFile),
hasMore: result.files.length >= PAGE_SIZE,
};
};

const createFolder = async (parentFolderUuid: string, name: string): Promise<void> => {
const sdk = SdkManager.getInstance();
const result = sdk.storageV2.createFolderByUuid({ parentFolderUuid, plainName: name });
if (!result) throw new Error('createFolder failed');
await result[0];
};

export const shareDriveService = { getFolderFolders, getFolderFiles, createFolder };
23 changes: 23 additions & 0 deletions src/shareExtension/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { File as FSFile } from 'expo-file-system';
import prettysize from 'prettysize';

dayjs.extend(relativeTime);

export const formatDate = (dateStr: string): string => {
const date = dayjs(dateStr);
const weekDays = 7;
return dayjs().diff(date, 'day') < weekDays ? date.fromNow() : date.toDate().toLocaleDateString();
};

export const readSize = (uri: string): number | null => {
try {
const file = new FSFile(uri);
return file.exists ? file.size : null;
} catch {
return null;
}
};

export const formatBytes = (bytes: number): string => prettysize(bytes);
Loading