1 import type React from 'react';
2 import { useEffect, useMemo } from 'react';
4 import { EVENT_TYPES } from '@proton/shared/lib/drive/constants';
6 import { sendErrorReport } from '../../utils/errorHandling';
7 import { EnrichedError } from '../../utils/errorHandling/EnrichedError';
8 import type { LinkDownload } from '../_downloads';
9 import { useDownloadProvider } from '../_downloads';
10 import type { DriveEvent, DriveEvents } from '../_events';
11 import { useDriveEventManager } from '../_events';
12 import { useLinksListing, useLinksQueue } from '../_links';
13 import { isPhotoGroup, sortWithCategories, usePhotos } from '../_photos';
14 import type { PhotoLink } from '../_photos';
15 import { useAbortSignal, useMemoArrayNoMatterTheOrder } from './utils';
18 * For Photos, we listen for delete and move events
19 * to update our internal state which is not linked to the global state.
21 export function updateByEvents(
22 { events, eventId }: DriveEvents,
24 removePhotosFromCache: (linkIds: string[]) => void,
25 processedEventCounter: (eventId: string, event: DriveEvent) => void
27 const linksToRemove = events
30 event.eventType === EVENT_TYPES.DELETE ||
31 (event.originShareId === shareId && event.encryptedLink.rootShareId !== event.originShareId)
34 processedEventCounter(eventId, event);
35 return event.encryptedLink.linkId;
38 removePhotosFromCache(linksToRemove);
41 export const usePhotosView = () => {
42 const eventsManager = useDriveEventManager();
43 const { getCachedChildren, loadLinksMeta } = useLinksListing();
44 const { shareId, linkId, isLoading, volumeId, photos, loadPhotos, removePhotosFromCache } = usePhotos();
45 const { addToQueue } = useLinksQueue({ loadThumbnails: true });
46 const { download } = useDownloadProvider();
48 const abortSignal = useAbortSignal([shareId, linkId]);
49 const cache = shareId && linkId ? getCachedChildren(abortSignal, shareId, linkId) : undefined;
50 const cachedLinks = useMemoArrayNoMatterTheOrder(cache?.links || []);
52 // This will be flattened to contain categories and links
53 const { photosViewData, photoLinkIdToIndexMap, photoLinkIds } = useMemo(() => {
54 if (!shareId || !linkId) {
57 photoLinkIdToIndexMap: {},
62 const result: Record<string, PhotoLink> = {};
64 // We create "fake" links to avoid complicating the rest of the code
65 photos.forEach((photo) => {
66 result[photo.linkId] = {
77 // Add data from cache
78 cachedLinks.forEach((link) => {
79 // If this link is not a photo, ignore it
80 if (!link.activeRevision?.photo) {
84 // Related photos are not supported by the web client for now
85 if (link.activeRevision.photo.mainPhotoLinkId) {
89 result[link.linkId] = link;
92 const photosViewData = sortWithCategories(Object.values(result));
94 // To improve performance, let's build some maps ahead of time
95 // For previews and selection, we need these maps to know where
96 // each link is located in the data array.
97 let photoLinkIdToIndexMap: Record<string, number> = {};
99 // We also provide a list of linkIds for the preview navigation,
100 // so it's important that this array follows the sorted view order.
101 let photoLinkIds: string[] = [];
103 photosViewData.forEach((item, index) => {
104 if (!isPhotoGroup(item)) {
105 photoLinkIdToIndexMap[item.linkId] = index;
106 photoLinkIds.push(item.linkId);
112 photoLinkIdToIndexMap,
115 }, [photos, cachedLinks, linkId, shareId]);
118 if (!volumeId || !shareId) {
121 const abortController = new AbortController();
123 loadPhotos(abortController.signal, volumeId);
125 const callbackId = eventsManager.eventHandlers.register((eventVolumeId, events, processedEventCounter) => {
126 if (eventVolumeId === volumeId) {
127 updateByEvents(events, shareId, removePhotosFromCache, processedEventCounter);
132 eventsManager.eventHandlers.unregister(callbackId);
133 abortController.abort();
135 }, [volumeId, shareId]);
137 const loadPhotoLink = (linkId: string, domRef?: React.MutableRefObject<unknown>) => {
142 addToQueue(shareId, linkId, domRef);
146 * A `PhotoLink` may not be fully loaded, so we need to preload all links in the cache
147 * first to request a download.
149 * @param linkIds List of Link IDs to preload
151 const requestDownload = async (linkIds: string[]) => {
156 const ac = new AbortController();
157 const meta = await loadLinksMeta(ac.signal, 'photos-download', shareId, linkIds);
159 if (meta.links.length === 0) {
163 if (meta.errors.length > 0) {
165 new EnrichedError('Failed to load links meta for download', {
170 linkIds: linkIds.filter((id) => !meta.links.find((link) => link.linkId === id)),
179 const relatedLinkIds = meta.links.flatMap((link) => link.activeRevision?.photo?.relatedPhotosLinkIds || []);
181 const relatedMeta = await loadLinksMeta(ac.signal, 'photos-related-download', shareId, relatedLinkIds);
183 if (relatedMeta.errors.length > 0) {
185 new EnrichedError('Failed to load links meta for download', {
190 linkIds: linkIds.filter((id) => !relatedMeta.links.find((link) => link.linkId === id)),
191 errors: relatedMeta.errors,
199 const links: LinkDownload[] = [...meta.links, ...relatedMeta.links].map(
203 shareId: link.rootShareId,
204 }) satisfies LinkDownload
207 await download(links);
213 photos: photosViewData,
214 photoLinkIdToIndexMap,
216 removePhotosFromCache,