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';
10 useConfirmActionModal,
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;
34 const MAX_VISIBLE_TRANSFERS_LARGE_SCREEN = 5;
35 const MAX_VISIBLE_TRANSFERS_SMALL_SCREEN = 3;
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];
54 progress: stats[transfer.id]?.progress ?? 0,
55 speed: stats[transfer.id]?.averageSpeed ?? 0,
61 const tabIndexToTransferGroup = {
63 1: TransferGroup.ACTIVE,
64 2: TransferGroup.DONE,
65 3: TransferGroup.FAILURE,
68 type TabIndices = keyof typeof tabIndexToTransferGroup;
70 const TransferManager = ({
76 numberOfFailedTransfer,
78 downloads: Download[];
80 stats: TransfersStats;
82 hasActiveTransfer: boolean;
83 numberOfFailedTransfer: {
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];
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
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
120 window.addEventListener('unload', onClear);
121 const unregister = busy.register();
123 window.removeEventListener('unload', onClear);
129 <T extends TransferType>(type: T) =>
130 (transfer: T extends TransferType.Download ? Download : Upload): TransferListEntry<T> => ({
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())
145 const transferGroupFilter = tabIndexToTransferGroup[activeTabIndex];
146 if (transferGroupFilter === undefined) {
149 return STATE_TO_GROUP_MAP[entry.transfer.state] === transferGroupFilter;
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`,
160 .t`There are files that still need to be transferred. Closing the transfer manager will end all operations.`,
161 onSubmit: async () => onClear(),
167 if (numberOfFailedTransfer.total) {
168 let title = c('Title').ngettext(
169 msgid`${numberOfFailedTransfer.total} failed transfer`,
170 `${numberOfFailedTransfer.total} failed transfers`,
171 numberOfFailedTransfer.total
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
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
187 message = c('Info').t`Some files failed to download. Try downloading the files again.`;
190 void showConfirmModal({
193 cancelText: c('Action').t`Retry`,
194 submitText: c('Action').t`Close`,
196 return transferManagerControls.restartTransfers(
197 entries.filter(({ transfer }) => {
198 return isTransferFailed(transfer);
202 onSubmit: async () => onClear(),
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);
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);
232 [windowHeight, minimized]
237 {entries.length === 0 && (
239 className="transfers-manager-list-placeholder flex justify-center items-center"
240 style={{ height: calcultateItemsHeight(1) }}
242 <span className="mb-4">{c('Info').t`No results found`} </span>
245 <div className="transfers-manager-list">
248 direction={isRTL ? 'rtl' : 'ltr'}
249 className="outline-none"
254 itemCount={entries.length}
255 itemSize={ROW_HEIGHT_PX}
256 height={calculateListHeight(entries.length)}
258 itemKey={(index, { entries }: ListItemData) => entries[index].transfer?.id ?? index}
270 id="transfer-manager"
271 className={clsx(['transfers-manager', minimized && 'transfers-manager--minimized'])}
273 <div ref={headerRef}>
275 downloads={downloads}
278 minimized={minimized}
279 onToggleMinimize={toggleMinimized}
280 onClose={handleCloseClick}
283 <div ref={containerRef} className="flex">
289 title: c('Title').t`All`,
293 title: c('Title').t`Active`,
297 title: c('Title').t`Completed`,
301 title: c('Title').t`Failed`,
305 value={activeTabIndex}
306 onChange={(groupValue) => {
307 setActiveTabIndex(groupValue as TabIndices);
310 {!viewportWidth['<=small'] && (
312 className="transfers-manager-header-buttons p-2 pr-4"
314 showDownloadLog={activeTabIndex === TransferGroup.FAILURE}
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
335 const TransferManagerContainer = () => {
336 const { downloads, uploads, hasActiveTransfer, numberOfFailedTransfer, stats, clearAllTransfers } =
339 if (!downloads.length && !uploads.length) {
345 downloads={downloads}
348 onClear={clearAllTransfers}
349 hasActiveTransfer={hasActiveTransfer}
350 numberOfFailedTransfer={numberOfFailedTransfer}
355 export default TransferManagerContainer;