Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / components / sections / Photos / PhotosView.tsx
blob87553cd431b7dce5f0fb8bc916cd8575af57f7c2
1 import type { FC } from 'react';
2 import React, { useCallback, useMemo, useRef, useState } from 'react';
4 import { c, msgid } from 'ttag';
6 import { Loader, NavigationControl, TopBanner, useAppTitle } from '@proton/components';
7 import { LayoutSetting } from '@proton/shared/lib/interfaces/drive/userSettings';
8 import { useFlag } from '@proton/unleash';
10 import { useOnItemRenderedMetrics } from '../../../hooks/drive/useOnItemRenderedMetrics';
11 import { useShiftKey } from '../../../hooks/util/useShiftKey';
12 import type { PhotoLink } from '../../../store';
13 import { isDecryptedLink, usePhotosView, useThumbnailsDownload } from '../../../store';
14 import PortalPreview from '../../PortalPreview';
15 import { useDetailsModal } from '../../modals/DetailsModal';
16 import { useLinkSharingModal } from '../../modals/ShareLinkModal/ShareLinkModal';
17 import UploadDragDrop from '../../uploads/UploadDragDrop/UploadDragDrop';
18 import ToolbarRow from '../ToolbarRow/ToolbarRow';
19 import { EmptyPhotos } from './EmptyPhotos';
20 import { PhotosGrid } from './PhotosGrid';
21 import { PhotosClearSelectionButton } from './components/PhotosClearSelectionButton';
22 import PhotosRecoveryBanner from './components/PhotosRecoveryBanner/PhotosRecoveryBanner';
23 import { usePhotosSelection } from './hooks';
24 import { PhotosToolbar } from './toolbar';
26 export const PhotosView: FC<void> = () => {
27     useAppTitle(c('Title').t`Photos`);
29     const isUploadDisabled = useFlag('DrivePhotosUploadDisabled');
30     const { shareId, linkId, photos, isLoading, loadPhotoLink, photoLinkIdToIndexMap, photoLinkIds, requestDownload } =
31         usePhotosView();
32     const { selectedItems, clearSelection, isGroupSelected, isItemSelected, handleSelection } = usePhotosSelection(
33         photos,
34         photoLinkIdToIndexMap
35     );
36     const { incrementItemRenderedCounter } = useOnItemRenderedMetrics(LayoutSetting.Grid, isLoading);
37     const [detailsModal, showDetailsModal] = useDetailsModal();
38     const [linkSharingModal, showLinkSharingModal] = useLinkSharingModal();
39     const [previewLinkId, setPreviewLinkId] = useState<string | undefined>();
40     const isShiftPressed = useShiftKey();
41     const thumbnails = useThumbnailsDownload();
43     const handleItemRender = useCallback(
44         (itemLinkId: string, domRef: React.MutableRefObject<unknown>) => {
45             incrementItemRenderedCounter();
46             loadPhotoLink(itemLinkId, domRef);
47         },
48         [incrementItemRenderedCounter, loadPhotoLink]
49     );
51     const handleItemRenderLoadedLink = (itemLinkId: string, domRef: React.MutableRefObject<unknown>) => {
52         if (shareId) {
53             thumbnails.addToDownloadQueue(shareId, itemLinkId, undefined, domRef);
54         }
55     };
57     const photoCount = photoLinkIds.length;
58     const selectedCount = selectedItems.length;
60     const handleToolbarPreview = useCallback(() => {
61         let selected = selectedItems[0];
63         if (selectedItems.length === 1 && selected) {
64             setPreviewLinkId(selected.linkId);
65         }
66     }, [selectedItems, setPreviewLinkId]);
68     const previewRef = useRef<HTMLDivElement>(null);
69     const previewIndex = useMemo(
70         () => photoLinkIds.findIndex((item) => item === previewLinkId),
71         [photoLinkIds, previewLinkId]
72     );
73     const previewItem = useMemo(
74         () => (previewLinkId !== undefined ? (photos[photoLinkIdToIndexMap[previewLinkId]] as PhotoLink) : undefined),
75         [photos, previewLinkId, photoLinkIdToIndexMap]
76     );
77     const setPreviewIndex = useCallback(
78         (index: number) => setPreviewLinkId(photoLinkIds[index]),
79         [setPreviewLinkId, photoLinkIds]
80     );
82     const isEmpty = photos.length === 0;
84     if (isLoading && isEmpty) {
85         return <Loader />;
86     }
88     if (!shareId || !linkId) {
89         return <EmptyPhotos />;
90     }
92     const hasPreview = !!previewItem;
94     return (
95         <>
96             {detailsModal}
97             {linkSharingModal}
98             {isUploadDisabled && (
99                 <TopBanner className="bg-warning">{c('Info')
100                     .t`We are experiencing technical issues. Uploading new photos is temporarily disabled.`}</TopBanner>
101             )}
102             {hasPreview && (
103                 <PortalPreview
104                     ref={previewRef}
105                     shareId={shareId}
106                     linkId={previewItem.linkId}
107                     revisionId={isDecryptedLink(previewItem) ? previewItem.activeRevision?.id : undefined}
108                     key="portal-preview-photos"
109                     open={hasPreview}
110                     date={
111                         previewItem.activeRevision?.photo?.captureTime ||
112                         (isDecryptedLink(previewItem) ? previewItem.createTime : undefined)
113                     }
114                     onShare={
115                         isDecryptedLink(previewItem) && previewItem?.trashed
116                             ? undefined
117                             : () => showLinkSharingModal({ shareId, linkId: previewItem.linkId })
118                     }
119                     onDetails={() =>
120                         showDetailsModal({
121                             shareId,
122                             linkId: previewItem.linkId,
123                         })
124                     }
125                     navigationControls={
126                         <NavigationControl
127                             current={previewIndex + 1}
128                             total={photoCount}
129                             rootRef={previewRef}
130                             onPrev={() => setPreviewIndex(previewIndex - 1)}
131                             onNext={() => setPreviewIndex(previewIndex + 1)}
132                         />
133                     }
134                     onClose={() => setPreviewLinkId(undefined)}
135                     onExit={() => setPreviewLinkId(undefined)}
136                 />
137             )}
138             <PhotosRecoveryBanner />
139             <UploadDragDrop
140                 disabled={isUploadDisabled}
141                 isForPhotos
142                 shareId={shareId}
143                 linkId={linkId}
144                 className="flex flex-column flex-nowrap flex-1"
145             >
146                 <ToolbarRow
147                     titleArea={
148                         <span className="flex items-center text-strong pl-1">
149                             {selectedCount > 0 ? (
150                                 <div className="flex gap-2" data-testid="photos-selected-count">
151                                     <PhotosClearSelectionButton onClick={clearSelection} />
152                                     {/* aria-live & aria-atomic ensure the count gets revocalized when it changes */}
153                                     <span aria-live="polite" aria-atomic="true">
154                                         {c('Info').ngettext(
155                                             msgid`${selectedCount} selected`,
156                                             `${selectedCount} selected`,
157                                             selectedCount
158                                         )}
159                                     </span>
160                                 </div>
161                             ) : (
162                                 c('Title').t`Photos`
163                             )}
164                             {isLoading && <Loader className="ml-2 flex items-center" />}
165                         </span>
166                     }
167                     toolbar={
168                         <PhotosToolbar
169                             shareId={shareId}
170                             linkId={linkId}
171                             selectedItems={selectedItems}
172                             onPreview={handleToolbarPreview}
173                             requestDownload={requestDownload}
174                             uploadDisabled={isUploadDisabled}
175                         />
176                     }
177                 />
179                 {isEmpty ? (
180                     <EmptyPhotos />
181                 ) : (
182                     <PhotosGrid
183                         data={photos}
184                         onItemRender={handleItemRender}
185                         onItemRenderLoadedLink={handleItemRenderLoadedLink}
186                         isLoading={isLoading}
187                         onItemClick={setPreviewLinkId}
188                         hasSelection={selectedCount > 0}
189                         onSelectChange={(i, isSelected) =>
190                             handleSelection(i, { isSelected, isMultiSelect: isShiftPressed() })
191                         }
192                         isGroupSelected={isGroupSelected}
193                         isItemSelected={isItemSelected}
194                     />
195                 )}
196             </UploadDragDrop>
197         </>
198     );