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';
9 ConflictStrategyHandler,
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
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 }>({});
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 = {};
49 }, [fileUploads, folderUploads]);
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
59 const getConflictHandler = useCallback(
61 conflictStrategyRef: React.MutableRefObject<{ [id: string]: TransferConflictStrategy }>,
63 ): ConflictStrategyHandler => {
64 return (abortSignal, originalIsDraft, originalIsFolder) => {
65 const getStrategy = (): TransferConflictStrategy | undefined => {
67 conflictStrategyRef.current[CONFLICT_STRATEGY_ALL_ID] || conflictStrategyRef.current[uploadId]
71 const strategy = getStrategy();
73 return Promise.resolve(strategy);
75 updateWithData(uploadId, TransferState.Conflict, { originalIsDraft, originalIsFolder });
77 return new Promise((resolve, reject) => {
78 waitUntil(() => !!getStrategy(), abortSignal)
80 const strategy = getStrategy() as TransferConflictStrategy;
84 reject(new TransferCancel({ message: 'Upload was canceled' }));
92 const getFileConflictHandler = useCallback(
93 (uploadId: string) => {
94 return getConflictHandler(fileConflictStrategy, uploadId);
99 const getFolderConflictHandler = useCallback(
100 (uploadId: string) => {
101 return getConflictHandler(folderConflictStrategy, uploadId);
106 const openConflictStrategyModal = (
108 conflictStrategyRef: React.MutableRefObject<{ [id: string]: TransferConflictStrategy }>,
112 originalIsDraft?: boolean;
113 originalIsFolder?: boolean;
114 isForPhotos?: boolean;
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;
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)) {
131 return isTransferConflict({ state });
132 }, TransferState.Progress);
134 updateState(uploadId, TransferState.Progress);
137 const cancelAll = () => {
138 isConflictStrategyModalOpen.current = false;
139 conflictStrategyRef.current[CONFLICT_STRATEGY_ALL_ID] = TransferConflictStrategy.Skip;
140 cancelUploads(isTransferActive);
142 showConflictModal({ apply, cancelAll, ...params });
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.
148 if (isConflictStrategyModalOpen.current) {
152 const conflictingFolderUpload = folderUploads.find(isTransferConflict);
153 if (conflictingFolderUpload) {
154 openConflictStrategyModal(conflictingFolderUpload.id, folderConflictStrategy, {
155 name: conflictingFolderUpload.meta.filename,
157 originalIsDraft: conflictingFolderUpload.originalIsDraft,
158 originalIsFolder: conflictingFolderUpload.originalIsFolder,
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,
172 }, [fileUploads, folderUploads]);
175 getFolderConflictHandler,
176 getFileConflictHandler,