Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _views / useTree.tsx
blobbd2d0badebc57a4caa012816757777e96e6be254
1 import { useCallback, useEffect, useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { useNotifications } from '@proton/components';
7 import type { DecryptedLink } from '../_links';
8 import { useLink, useLinksListing } from '../_links';
9 import { ShareType, useShare } from '../_shares';
10 import { useErrorHandler } from '../_utils';
11 import { useAbortSignal } from './utils';
12 import { useShareType } from './utils/useShareType';
14 export interface TreeItem {
15     link: DecryptedLink;
16     isExpanded: boolean;
17     isLoaded: boolean;
18     children: TreeItem[];
21 interface TreeOptions {
22     rootLinkId?: string;
23     rootExpanded?: boolean;
24     foldersOnly?: boolean;
27 export function useTreeForModals(shareId: string, options?: Omit<TreeOptions, 'rootLinkId'>) {
28     const shareType = useShareType(shareId);
29     const getRootItems = (tree: ReturnType<typeof useTree>): TreeItem[] => {
30         if (shareType === ShareType.device) {
31             return tree.rootFolder?.children ? tree.rootFolder?.children : [];
32         }
34         const isLoaded = tree.rootFolder?.isLoaded;
36         if (isLoaded && tree.rootFolder?.children.length === 0) {
37             // Avoid displaying root folder for empty My Files section not
38             // to have only one non-interactable tree element in the UI
39             return [];
40         }
42         return tree.rootFolder ? [tree.rootFolder] : [];
43     };
45     const tree = useTree(shareId, { ...options });
46     const isLoaded = tree.rootFolder?.isLoaded || false;
48     let items = getRootItems(tree);
50     return {
51         ...tree,
52         rootItems: items,
53         isLoaded,
54     };
57 /**
58  * useFolderTree provides data for folder tree view of the provided share.
59  *
60  * @deprecated – if possible, reuse logic from useTreeForModals, if there's a need to
61  * exlude root link from the output. The reason this function exists is that Sidebar
62  * component has it's own implementation of formatting the tree. This function and
63  * `useTreeForModals` is an object of possible refactor – the goal of it is to create
64  * a unified mechanism to get tree data ready for presentation.
65  */
66 export function useFolderTree(shareId: string, options?: TreeOptions) {
67     return useTree(shareId, { ...options, foldersOnly: true });
70 /**
71  * useTree provides data for complete tree view of the provided share.
72  */
73 export function useTree(shareId: string, { rootLinkId, rootExpanded, foldersOnly = false }: TreeOptions) {
74     const { createNotification } = useNotifications();
75     const { showErrorNotification } = useErrorHandler();
76     const { getShare } = useShare();
77     const { getLink } = useLink();
78     const { loadChildren, getCachedChildren } = useLinksListing();
80     const abortSignal = useAbortSignal([shareId, rootLinkId]);
81     const [rootFolder, setFolderTree] = useState<TreeItem>();
83     const getShareRootLinkId = async (abortSignal: AbortSignal, shareId: string) => {
84         const share = await getShare(abortSignal, shareId);
85         return share.rootLinkId;
86     };
88     // Reset the whole tree when share info changed.
89     useEffect(() => {
90         const abortController = new AbortController();
91         (rootLinkId ? Promise.resolve(rootLinkId) : getShareRootLinkId(abortController.signal, shareId))
92             .then((rootLinkId) => getLink(abortController.signal, shareId, rootLinkId))
93             .then((link) => {
94                 setFolderTree({
95                     link,
96                     isExpanded: rootExpanded || false,
97                     isLoaded: false,
98                     children: [],
99                 });
100             })
101             .catch((err) => {
102                 showErrorNotification(err, c('Notification').t`Root folder failed to be loaded`);
103             });
104     }, [shareId, rootLinkId, rootExpanded]);
106     const syncTreeWithCache = useCallback(
107         (item: TreeItem): TreeItem => {
108             // Sync with cache only expanded part of the tree so we don't have
109             // to keep in sync everything in the cache as that would need to
110             // make sure have everyting up to date and decrypted. If user don't
111             // need it, lets not waste valuable CPU time on it. But do it only
112             // for children - lets keep root folder always up to date, as we
113             // preload root everytime and the main expand button depends on it.
114             if (!item.isExpanded && item.link.parentLinkId) {
115                 return item;
116             }
118             const { links: children } = getCachedChildren(abortSignal, shareId, item.link.linkId, foldersOnly);
119             if (!children) {
120                 item.children = [];
121             } else {
122                 const newIds = children.map(({ linkId }) => linkId);
123                 const prevItems = item.isLoaded
124                     ? item.children
125                           .filter(({ link }) => newIds.includes(link.linkId))
126                           .map((child): TreeItem => {
127                               const item = children.find((item) => item.linkId === child.link.linkId);
128                               if (item && item.name !== child.link.name) {
129                                   return { ...child, link: item };
130                               }
131                               return child;
132                           })
133                     : item.children;
135                 const currentIds = item.children.map(({ link }) => link.linkId);
136                 const newItems = children
137                     .filter((item) => !currentIds.includes(item.linkId) && !item.trashed)
138                     .map(
139                         (item): TreeItem => ({
140                             link: item,
141                             isExpanded: false,
142                             isLoaded: false,
143                             children: [],
144                         })
145                     );
147                 item.children = [...prevItems, ...newItems].map(syncTreeWithCache);
148                 item.children.sort((a, b) => {
149                     if (a.link.isFile !== b.link.isFile) {
150                         return a.link.isFile < b.link.isFile ? -1 : 1;
151                     }
152                     return a.link.name.localeCompare(b.link.name, undefined, { numeric: true });
153                 });
154             }
155             return { ...item };
156         },
157         [shareId, foldersOnly, getCachedChildren]
158     );
160     const setLoadedFlag = (item: TreeItem, linkId: string) => {
161         if (item.link.linkId === linkId) {
162             item.isLoaded = true;
163             return { ...item };
164         }
165         item.children = item.children.map((child) => setLoadedFlag(child, linkId));
166         return item;
167     };
169     const loadSubfolders = useCallback(
170         (abortSignal: AbortSignal, linkId: string) => {
171             loadChildren(abortSignal, shareId, linkId, foldersOnly)
172                 .then(() => {
173                     setFolderTree((state) => {
174                         if (!state) {
175                             return;
176                         }
177                         state = setLoadedFlag(state, linkId);
178                         return syncTreeWithCache(state);
179                     });
180                 })
181                 .catch((err) => showErrorNotification(err, c('Notification').t`Subfolder failed to be loaded`));
182         },
183         [shareId, foldersOnly, loadChildren]
184     );
186     // Update local folder tree when cache has updated.
187     useEffect(() => {
188         setFolderTree((state) => (state ? syncTreeWithCache(state) : undefined));
189     }, [syncTreeWithCache]);
191     // Load root childs automatically so we have anything to show right away.
192     useEffect(() => {
193         if (!rootFolder || rootFolder.isLoaded) {
194             return;
195         }
197         const abortController = new AbortController();
198         loadSubfolders(abortController.signal, rootFolder.link.linkId);
199         return () => {
200             abortController.abort();
201         };
202     }, [!rootFolder || rootFolder.isLoaded, rootFolder?.link?.linkId]);
204     const setExpand = useCallback(
205         (linkId: string, getNewExpanded: (item: TreeItem) => boolean) => {
206             const updateExpand = (item: TreeItem) => {
207                 if (item.link.linkId === linkId) {
208                     if (!item.isExpanded && !item.isLoaded) {
209                         loadSubfolders(new AbortController().signal, item.link.linkId);
210                     }
211                     item.isExpanded = getNewExpanded(item);
212                 }
213                 item.children = item.children.map(updateExpand);
214                 return { ...item };
215             };
216             setFolderTree((state) => (state ? updateExpand(state) : undefined));
217         },
218         [loadSubfolders]
219     );
221     const expand = useCallback((linkId: string) => setExpand(linkId, () => true), [setExpand]);
223     const toggleExpand = useCallback(
224         (linkId: string) => setExpand(linkId, ({ isExpanded }) => !isExpanded),
225         [setExpand]
226     );
228     const deepestOpenedLevel = useMemo(() => getDeepestOpenedLevel(rootFolder), [rootFolder]);
230     useEffect(() => {
231         if (deepestOpenedLevel === 42) {
232             createNotification({
233                 type: 'info',
234                 text: 'Achievement unlocked: folder tree master 1',
235             });
236         }
237     }, [deepestOpenedLevel]);
239     return {
240         deepestOpenedLevel,
241         rootFolder,
242         expand,
243         toggleExpand,
244     };
247 function getDeepestOpenedLevel(item?: TreeItem, level = 0): number {
248     if (!item || !item.isExpanded || !item.children.length) {
249         return level;
250     }
251     const levels = item.children.map((child) => getDeepestOpenedLevel(child, level + 1));
252     return Math.max(...levels);