Merge branch 'comment-composer' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / UploadProvider / useUploadConflict.tsx
blobdfe14449d546433eb8bdb7831a3827fc8e27dffd
1 import { useCallback, useEffect, useRef } from 'react';
3 import { TransferCancel, TransferState } from '../../../components/TransferManager/transfer';
4 import { useConflictModal } from '../../../components/modals/ConflictModal';
5 import { waitUntil } from '../../../utils/async';
6 import { isTransferActive, isTransferConflict } from '../../../utils/transfer';
7 import { TransferConflictStrategy } from '../interface';
8 import type {
9     ConflictStrategyHandler,
10     FileUpload,
11     FolderUpload,
12     UpdateData,
13     UpdateFilter,
14     UpdateState,
15 } from './interface';
17 // Empty string is ensured to not conflict with any upload ID or folder name.
18 // No upload has empty ID.
19 const CONFLICT_STRATEGY_ALL_ID = '';
21 export default function useUploadConflict(
22     fileUploads: FileUpload[],
23     folderUploads: FolderUpload[],
24     updateState: (filter: UpdateFilter, newState: UpdateState) => void,
25     updateWithData: (filter: UpdateFilter, newState: UpdateState, data: UpdateData) => void,
26     cancelUploads: (filter: UpdateFilter) => void
27 ) {
28     const [conflictModal, showConflictModal] = useConflictModal();
30     // There should be always visible only one modal to chose conflict strategy.
31     const isConflictStrategyModalOpen = useRef(false);
33     // Conflict strategy is set per upload, or CONFLICT_STRATEGY_ALL_ID is used
34     // to handle selection for all uploads.
35     // Strategies are cleared once all uploads are finished so user is asked
36     // again (consider that user could do another upload after an hour).
37     const fileConflictStrategy = useRef<{ [id: string]: TransferConflictStrategy }>({});
38     const folderConflictStrategy = useRef<{ [id: string]: TransferConflictStrategy }>({});
40     useEffect(() => {
41         // "Apply to all" should be active till the last transfer is active.
42         // Once all transfers finish, user can start another minutes or hours
43         // later and that means we should ask again.
44         const hasNoActiveUpload = ![...fileUploads, ...folderUploads].find(isTransferActive);
45         if (hasNoActiveUpload) {
46             fileConflictStrategy.current = {};
47             folderConflictStrategy.current = {};
48         }
49     }, [fileUploads, folderUploads]);
51     /**
52      * getConflictHandler returns handler which either returns the strategy
53      * right away, or it sets the state of the upload to conflict which will
54      * open ConflictModal to ask user what to do next. Handler waits till the
55      * user selects the strategy, and also any other upload is not started
56      * in case user applies the selection for all transfers, which might be
57      * even to cancel all.
58      */
59     const getConflictHandler = useCallback(
60         (
61             conflictStrategyRef: React.MutableRefObject<{ [id: string]: TransferConflictStrategy }>,
62             uploadId: string
63         ): ConflictStrategyHandler => {
64             return (abortSignal, originalIsDraft, originalIsFolder) => {
65                 const getStrategy = (): TransferConflictStrategy | undefined => {
66                     return (
67                         conflictStrategyRef.current[CONFLICT_STRATEGY_ALL_ID] || conflictStrategyRef.current[uploadId]
68                     );
69                 };
71                 const strategy = getStrategy();
72                 if (strategy) {
73                     return Promise.resolve(strategy);
74                 }
75                 updateWithData(uploadId, TransferState.Conflict, { originalIsDraft, originalIsFolder });
77                 return new Promise((resolve, reject) => {
78                     waitUntil(() => !!getStrategy(), abortSignal)
79                         .then(() => {
80                             const strategy = getStrategy() as TransferConflictStrategy;
81                             resolve(strategy);
82                         })
83                         .catch(() => {
84                             reject(new TransferCancel({ message: 'Upload was canceled' }));
85                         });
86                 });
87             };
88         },
89         [updateWithData]
90     );
92     const getFileConflictHandler = useCallback(
93         (uploadId: string) => {
94             return getConflictHandler(fileConflictStrategy, uploadId);
95         },
96         [getConflictHandler]
97     );
99     const getFolderConflictHandler = useCallback(
100         (uploadId: string) => {
101             return getConflictHandler(folderConflictStrategy, uploadId);
102         },
103         [getConflictHandler]
104     );
106     const openConflictStrategyModal = (
107         uploadId: string,
108         conflictStrategyRef: React.MutableRefObject<{ [id: string]: TransferConflictStrategy }>,
109         params: {
110             name: string;
111             isFolder?: boolean;
112             originalIsDraft?: boolean;
113             originalIsFolder?: boolean;
114             isForPhotos?: boolean;
115         }
116     ) => {
117         isConflictStrategyModalOpen.current = true;
119         const apply = (strategy: TransferConflictStrategy, all: boolean) => {
120             isConflictStrategyModalOpen.current = false;
121             conflictStrategyRef.current[all ? CONFLICT_STRATEGY_ALL_ID : uploadId] = strategy;
123             if (all) {
124                 updateState(({ state, file }) => {
125                     // Update only folders for folder conflict strategy.
126                     // And only files for file conflict strategy.
127                     const isFolder = file === undefined;
128                     if (isFolder !== (params.isFolder || false)) {
129                         return false;
130                     }
131                     return isTransferConflict({ state });
132                 }, TransferState.Progress);
133             } else {
134                 updateState(uploadId, TransferState.Progress);
135             }
136         };
137         const cancelAll = () => {
138             isConflictStrategyModalOpen.current = false;
139             conflictStrategyRef.current[CONFLICT_STRATEGY_ALL_ID] = TransferConflictStrategy.Skip;
140             cancelUploads(isTransferActive);
141         };
142         showConflictModal({ apply, cancelAll, ...params });
143     };
145     // Modals are openned on this one place only to not have race condition
146     // issue and ensure only one modal, either for file or folder, is openned.
147     useEffect(() => {
148         if (isConflictStrategyModalOpen.current) {
149             return;
150         }
152         const conflictingFolderUpload = folderUploads.find(isTransferConflict);
153         if (conflictingFolderUpload) {
154             openConflictStrategyModal(conflictingFolderUpload.id, folderConflictStrategy, {
155                 name: conflictingFolderUpload.meta.filename,
156                 isFolder: true,
157                 originalIsDraft: conflictingFolderUpload.originalIsDraft,
158                 originalIsFolder: conflictingFolderUpload.originalIsFolder,
159             });
160             return;
161         }
163         const conflictingFileUpload = fileUploads.find(isTransferConflict);
164         if (conflictingFileUpload) {
165             openConflictStrategyModal(conflictingFileUpload.id, fileConflictStrategy, {
166                 name: conflictingFileUpload.meta.filename,
167                 originalIsDraft: conflictingFileUpload.originalIsDraft,
168                 originalIsFolder: conflictingFileUpload.originalIsFolder,
169                 isForPhotos: conflictingFileUpload.isForPhotos,
170             });
171         }
172     }, [fileUploads, folderUploads]);
174     return {
175         getFolderConflictHandler,
176         getFileConflictHandler,
177         conflictModal,
178     };