Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / components / TransferManager / TransferManager.tsx
blob3a0e62214bd8f003ee9752dfc682bbcf284e2473
1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2 import type { ListChildComponentProps } from 'react-window';
3 import { FixedSizeList } from 'react-window';
5 import { c, msgid } from 'ttag';
7 import {
8     Tabs,
9     useActiveBreakpoint,
10     useConfirmActionModal,
11     useElementRect,
12     useRightToLeft,
13     useToggle,
14     useWindowSize,
15 } from '@proton/components';
16 import busy from '@proton/shared/lib/busy';
17 import { rootFontSize } from '@proton/shared/lib/helpers/dom';
18 import clsx from '@proton/utils/clsx';
20 import { useTransfersView } from '../../store';
21 import { isTransferFailed } from '../../utils/transfer';
22 import Header from './Header';
23 import HeaderButtons from './HeaderButtons';
24 import Transfer from './TransferItem';
25 import type { Download, TransfersStats, Upload } from './transfer';
26 import { STATE_TO_GROUP_MAP, TransferGroup, TransferType } from './transfer';
27 import useTransferControls from './useTransferControls';
29 interface TransferListEntry<T extends TransferType> {
30     transfer: T extends TransferType.Download ? Download : Upload;
31     type: T;
34 const MAX_VISIBLE_TRANSFERS_LARGE_SCREEN = 5;
35 const MAX_VISIBLE_TRANSFERS_SMALL_SCREEN = 3;
37 type ListItemData = {
38     entries: (TransferListEntry<TransferType.Download> | TransferListEntry<TransferType.Upload>)[];
39     stats: TransfersStats;
42 type ListItemRowProps = Omit<ListChildComponentProps, 'data'> & { data: ListItemData };
44 const ListItemRow = ({ style, index, data }: ListItemRowProps) => {
45     const { stats, entries } = data;
46     const { transfer, type } = entries[index];
48     return (
49         <Transfer
50             style={style}
51             transfer={transfer}
52             type={type}
53             stats={{
54                 progress: stats[transfer.id]?.progress ?? 0,
55                 speed: stats[transfer.id]?.averageSpeed ?? 0,
56             }}
57         />
58     );
61 const tabIndexToTransferGroup = {
62     0: undefined,
63     1: TransferGroup.ACTIVE,
64     2: TransferGroup.DONE,
65     3: TransferGroup.FAILURE,
68 type TabIndices = keyof typeof tabIndexToTransferGroup;
70 const TransferManager = ({
71     downloads,
72     uploads,
73     stats,
74     onClear,
75     hasActiveTransfer,
76     numberOfFailedTransfer,
77 }: {
78     downloads: Download[];
79     uploads: Upload[];
80     stats: TransfersStats;
81     onClear: () => void;
82     hasActiveTransfer: boolean;
83     numberOfFailedTransfer: {
84         total: number;
85         downloads: number;
86         uploads: number;
87     };
88 }) => {
89     const transferManagerControls = useTransferControls();
91     const containerRef = useRef<HTMLDivElement>(null);
92     const headerRef = useRef<HTMLDivElement>(null);
94     const [activeTabIndex, setActiveTabIndex] = useState<TabIndices>(0);
95     const windowHeight = useWindowSize()[1];
96     /*
97         FixedSizedList (used for virtual scrolling) requires `width` prop to work
98         correcty. This is why we use 'useElementRect' hook here.
99         `useElementRect` utilizes `getBoundingClientRect` method to get dimensions
100         of a given element reference.
102         *WARNING:* don't introduce conditional rendering to this component –
103         this will lead to a race condition in which an element could be removed
104         from DOM while React still keeping its reference. In this case
105         `getBoundingClientRect` returns zero for each target's dimension, which in certain
106         cases (here due to useElementRect specifics) cause rendering bugs.
108         For more details see TransferManagerContainer component
109     */
110     const rect = useElementRect(containerRef);
111     const rectHeader = useElementRect(headerRef);
112     const { state: minimized, toggle: toggleMinimized } = useToggle();
113     const [confirmModal, showConfirmModal] = useConfirmActionModal();
114     const { viewportWidth } = useActiveBreakpoint();
115     const [isRTL] = useRightToLeft();
117     const ROW_HEIGHT_PX = 4.375 * rootFontSize(); // 4.375 * 16 =  we want 70px by default
119     useEffect(() => {
120         window.addEventListener('unload', onClear);
121         const unregister = busy.register();
122         return () => {
123             window.removeEventListener('unload', onClear);
124             unregister();
125         };
126     }, [onClear]);
128     const getListEntry =
129         <T extends TransferType>(type: T) =>
130         (transfer: T extends TransferType.Download ? Download : Upload): TransferListEntry<T> => ({
131             transfer,
132             type,
133         });
135     const getDownloadListEntry = getListEntry(TransferType.Download);
136     const getUploadListEntry = getListEntry(TransferType.Upload);
138     const downloadEntries = useMemo(() => downloads.map(getDownloadListEntry), [downloads]);
139     const uploadEntries = useMemo(() => uploads.map(getUploadListEntry), [uploads]);
141     const entries = useMemo(() => {
142         return [...downloadEntries, ...uploadEntries]
143             .sort((a, b) => b.transfer.startDate.getTime() - a.transfer.startDate.getTime())
144             .filter((entry) => {
145                 const transferGroupFilter = tabIndexToTransferGroup[activeTabIndex];
146                 if (transferGroupFilter === undefined) {
147                     return true;
148                 }
149                 return STATE_TO_GROUP_MAP[entry.transfer.state] === transferGroupFilter;
150             });
151     }, [downloadEntries, uploadEntries, activeTabIndex]);
153     const handleCloseClick = () => {
154         if (hasActiveTransfer) {
155             void showConfirmModal({
156                 title: c('Title').t`Stop transfers?`,
157                 cancelText: c('Action').t`Continue transfers`,
158                 submitText: c('Action').t`Stop transfers`,
159                 message: c('Info')
160                     .t`There are files that still need to be transferred. Closing the transfer manager will end all operations.`,
161                 onSubmit: async () => onClear(),
162                 canUndo: true,
163             });
164             return;
165         }
167         if (numberOfFailedTransfer.total) {
168             let title = c('Title').ngettext(
169                 msgid`${numberOfFailedTransfer.total} failed transfer`,
170                 `${numberOfFailedTransfer.total} failed transfers`,
171                 numberOfFailedTransfer.total
172             );
173             let message = c('Info').t`Not all files were transferred. Try uploading or downloading the files again.`;
174             if (numberOfFailedTransfer.uploads && !numberOfFailedTransfer.downloads) {
175                 title = c('Title').ngettext(
176                     msgid`${numberOfFailedTransfer.total} failed upload`,
177                     `${numberOfFailedTransfer.total} failed uploads`,
178                     numberOfFailedTransfer.total
179                 );
180                 message = c('Info').t`Some files failed to upload. Try uploading the files again.`;
181             } else if (!numberOfFailedTransfer.uploads && numberOfFailedTransfer.downloads) {
182                 title = c('Title').ngettext(
183                     msgid`${numberOfFailedTransfer.total} failed download`,
184                     `${numberOfFailedTransfer.total} failed downloads`,
185                     numberOfFailedTransfer.total
186                 );
187                 message = c('Info').t`Some files failed to download. Try downloading the files again.`;
188             }
190             void showConfirmModal({
191                 title,
192                 message,
193                 cancelText: c('Action').t`Retry`,
194                 submitText: c('Action').t`Close`,
195                 onCancel: () => {
196                     return transferManagerControls.restartTransfers(
197                         entries.filter(({ transfer }) => {
198                             return isTransferFailed(transfer);
199                         })
200                     );
201                 },
202                 onSubmit: async () => onClear(),
203                 canUndo: true,
204             });
205             return;
206         }
208         onClear();
209     };
211     const maxVisibleTransfers = viewportWidth['<=small']
212         ? MAX_VISIBLE_TRANSFERS_SMALL_SCREEN
213         : MAX_VISIBLE_TRANSFERS_LARGE_SCREEN;
215     const calcultateItemsHeight = useCallback(
216         (itemCount: number) => {
217             return ROW_HEIGHT_PX * Math.min(maxVisibleTransfers, itemCount);
218         },
219         [entries, minimized]
220     );
222     const calculateListHeight = useCallback(
223         (itemCount: number) => {
224             const itemsHeight = calcultateItemsHeight(itemCount);
226             if (itemsHeight + (rectHeader?.height || 0) > windowHeight) {
227                 return windowHeight - (rectHeader?.height || 0);
228             }
230             return itemsHeight;
231         },
232         [windowHeight, minimized]
233     );
235     const Content = (
236         <>
237             {entries.length === 0 && (
238                 <div
239                     className="transfers-manager-list-placeholder flex justify-center items-center"
240                     style={{ height: calcultateItemsHeight(1) }}
241                 >
242                     <span className="mb-4">{c('Info').t`No results found`} </span>
243                 </div>
244             )}
245             <div className="transfers-manager-list">
246                 {rect && (
247                     <FixedSizeList
248                         direction={isRTL ? 'rtl' : 'ltr'}
249                         className="outline-none"
250                         itemData={{
251                             entries,
252                             stats,
253                         }}
254                         itemCount={entries.length}
255                         itemSize={ROW_HEIGHT_PX}
256                         height={calculateListHeight(entries.length)}
257                         width={rect.width}
258                         itemKey={(index, { entries }: ListItemData) => entries[index].transfer?.id ?? index}
259                     >
260                         {ListItemRow}
261                     </FixedSizeList>
262                 )}
263             </div>
264         </>
265     );
267     return (
268         <>
269             <div
270                 id="transfer-manager"
271                 className={clsx(['transfers-manager', minimized && 'transfers-manager--minimized'])}
272             >
273                 <div ref={headerRef}>
274                     <Header
275                         downloads={downloads}
276                         uploads={uploads}
277                         stats={stats}
278                         minimized={minimized}
279                         onToggleMinimize={toggleMinimized}
280                         onClose={handleCloseClick}
281                     />
282                 </div>
283                 <div ref={containerRef} className="flex">
284                     {!minimized && (
285                         <>
286                             <Tabs
287                                 tabs={[
288                                     {
289                                         title: c('Title').t`All`,
290                                         content: Content,
291                                     },
292                                     {
293                                         title: c('Title').t`Active`,
294                                         content: Content,
295                                     },
296                                     {
297                                         title: c('Title').t`Completed`,
298                                         content: Content,
299                                     },
300                                     {
301                                         title: c('Title').t`Failed`,
302                                         content: Content,
303                                     },
304                                 ]}
305                                 value={activeTabIndex}
306                                 onChange={(groupValue) => {
307                                     setActiveTabIndex(groupValue as TabIndices);
308                                 }}
309                             />
310                             {!viewportWidth['<=small'] && (
311                                 <HeaderButtons
312                                     className="transfers-manager-header-buttons p-2 pr-4"
313                                     entries={entries}
314                                     showDownloadLog={activeTabIndex === TransferGroup.FAILURE}
315                                 />
316                             )}
317                         </>
318                     )}
319                 </div>
320             </div>
321             {confirmModal}
322         </>
323     );
327  * This component is introduced specifically to address the race condition of
328  * `return null` code branch caused by `clearAllTransfers` call and width
329  * calculation inside `TransferManager`.
331  * Separating this chunk of code into its component guaranties that
332  * list element will be *always* present in DOM for correct transfer manager list
333  * width calculation.
334  */
335 const TransferManagerContainer = () => {
336     const { downloads, uploads, hasActiveTransfer, numberOfFailedTransfer, stats, clearAllTransfers } =
337         useTransfersView();
339     if (!downloads.length && !uploads.length) {
340         return null;
341     }
343     return (
344         <TransferManager
345             downloads={downloads}
346             uploads={uploads}
347             stats={stats}
348             onClear={clearAllTransfers}
349             hasActiveTransfer={hasActiveTransfer}
350             numberOfFailedTransfer={numberOfFailedTransfer}
351         />
352     );
355 export default TransferManagerContainer;