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 } =
32 const { selectedItems, clearSelection, isGroupSelected, isItemSelected, handleSelection } = usePhotosSelection(
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);
48 [incrementItemRenderedCounter, loadPhotoLink]
51 const handleItemRenderLoadedLink = (itemLinkId: string, domRef: React.MutableRefObject<unknown>) => {
53 thumbnails.addToDownloadQueue(shareId, itemLinkId, undefined, domRef);
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);
66 }, [selectedItems, setPreviewLinkId]);
68 const previewRef = useRef<HTMLDivElement>(null);
69 const previewIndex = useMemo(
70 () => photoLinkIds.findIndex((item) => item === previewLinkId),
71 [photoLinkIds, previewLinkId]
73 const previewItem = useMemo(
74 () => (previewLinkId !== undefined ? (photos[photoLinkIdToIndexMap[previewLinkId]] as PhotoLink) : undefined),
75 [photos, previewLinkId, photoLinkIdToIndexMap]
77 const setPreviewIndex = useCallback(
78 (index: number) => setPreviewLinkId(photoLinkIds[index]),
79 [setPreviewLinkId, photoLinkIds]
82 const isEmpty = photos.length === 0;
84 if (isLoading && isEmpty) {
88 if (!shareId || !linkId) {
89 return <EmptyPhotos />;
92 const hasPreview = !!previewItem;
98 {isUploadDisabled && (
99 <TopBanner className="bg-warning">{c('Info')
100 .t`We are experiencing technical issues. Uploading new photos is temporarily disabled.`}</TopBanner>
106 linkId={previewItem.linkId}
107 revisionId={isDecryptedLink(previewItem) ? previewItem.activeRevision?.id : undefined}
108 key="portal-preview-photos"
111 previewItem.activeRevision?.photo?.captureTime ||
112 (isDecryptedLink(previewItem) ? previewItem.createTime : undefined)
115 isDecryptedLink(previewItem) && previewItem?.trashed
117 : () => showLinkSharingModal({ shareId, linkId: previewItem.linkId })
122 linkId: previewItem.linkId,
127 current={previewIndex + 1}
130 onPrev={() => setPreviewIndex(previewIndex - 1)}
131 onNext={() => setPreviewIndex(previewIndex + 1)}
134 onClose={() => setPreviewLinkId(undefined)}
135 onExit={() => setPreviewLinkId(undefined)}
138 <PhotosRecoveryBanner />
140 disabled={isUploadDisabled}
144 className="flex flex-column flex-nowrap flex-1"
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">
155 msgid`${selectedCount} selected`,
156 `${selectedCount} selected`,
164 {isLoading && <Loader className="ml-2 flex items-center" />}
171 selectedItems={selectedItems}
172 onPreview={handleToolbarPreview}
173 requestDownload={requestDownload}
174 uploadDisabled={isUploadDisabled}
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() })
192 isGroupSelected={isGroupSelected}
193 isItemSelected={isItemSelected}