diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index 1fe3022dff..940c25f5fa 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -227,33 +227,24 @@ export const FileList: React.FC = ({ const [_items, setItems] = useState([]); const items = useDeferredValue(_items); - const listRef = useRef(null); - + const [rangeStartIndex, setRangeStartIndex] = useState( + undefined, + ); + const [hoverIndex, setHoverIndex] = useState(undefined); + const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); // Timeline date strings for which all photos have been selected. // // See: [Note: Timeline date string] const [checkedTimelineDateStrings, setCheckedTimelineDateStrings] = - useState(new Set()); + useState(new Set()); - const [rangeStart, setRangeStart] = useState(null); - const [currentHover, setCurrentHover] = useState(null); - const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); + const listRef = useRef(null); const layoutParams = useMemo( () => computeThumbnailGridLayoutParams(width), [width], ); - const { - // containerWidth, - // isSmallerLayout, - // paddingInline, - columns, - // itemWidth, - // itemHeight, - // gap, - } = layoutParams; - useEffect(() => { // Since width and height are dependencies, there might be too many // updates to the list during a resize. The list computation too, while @@ -326,11 +317,9 @@ export const FileList: React.FC = ({ } } - if (!isSmallerLayout) { - items = mergeTimeStampList(items, columns); - } + if (!isSmallerLayout) items = mergeRowsWherePossible(items, columns); - if (annotatedFiles.length == 0) { + if (!annotatedFiles.length) { items.push({ height: height - 48, tag: "span", @@ -409,33 +398,40 @@ export const FileList: React.FC = ({ }); }, [annotatedFiles, selected]); - const handleSelectMulti = handleSelectCreatorMulti( - setSelected, - mode, - user?.id, - activeCollectionID, - activePersonID, + const handleSelectMulti = useMemo( + () => + handleSelectCreatorMulti( + setSelected, + mode, + user?.id, + activeCollectionID, + activePersonID, + ), + [setSelected, mode, user?.id, activeCollectionID, activePersonID], ); - const onChangeSelectAllCheckBox = (date: string) => { - const next = new Set(checkedTimelineDateStrings); - let isDateSelected: boolean; - if (!next.has(date)) { - next.add(date); - isDateSelected = true; - } else { - next.delete(date); - isDateSelected = false; - } - setCheckedTimelineDateStrings(next); + const onChangeSelectAllCheckBox = useCallback( + (date: string) => { + const next = new Set(checkedTimelineDateStrings); + let isDateSelected: boolean; + if (!next.has(date)) { + next.add(date); + isDateSelected = true; + } else { + next.delete(date); + isDateSelected = false; + } + setCheckedTimelineDateStrings(next); - // All files on a checked/unchecked day. - const filesOnADay = annotatedFiles.filter( - (af) => af.timelineDateString === date, - ); + // All files on a checked/unchecked day. + const filesOnADay = annotatedFiles.filter( + (af) => af.timelineDateString === date, + ); - handleSelectMulti(filesOnADay.map((af) => af.file))(isDateSelected); - }; + handleSelectMulti(filesOnADay.map((af) => af.file))(isDateSelected); + }, + [annotatedFiles, checkedTimelineDateStrings, handleSelectMulti], + ); const handleSelect = useMemo( () => @@ -445,37 +441,36 @@ export const FileList: React.FC = ({ user?.id, activeCollectionID, activePersonID, - setRangeStart, + setRangeStartIndex, ), [setSelected, mode, user?.id, activeCollectionID, activePersonID], ); - const onHoverOver = (index: number) => () => { - setCurrentHover(index); - }; + const handleRangeSelect = useCallback( + (index: number) => { + if (rangeStartIndex === undefined || rangeStartIndex == index) + return; - const handleRangeSelect = (index: number) => () => { - if (typeof rangeStart != "undefined" && rangeStart !== index) { - const direction = - (index - rangeStart!) / Math.abs(index - rangeStart!); + const direction = index > rangeStartIndex ? 1 : -1; let checked = true; for ( - let i = rangeStart!; + let i = rangeStartIndex; (index - i) * direction >= 0; i += direction ) { checked = checked && !!selected[annotatedFiles[i]!.file.id]; } for ( - let i = rangeStart!; + let i = rangeStartIndex; (index - i) * direction > 0; i += direction ) { handleSelect(annotatedFiles[i]!.file)(!checked); } handleSelect(annotatedFiles[index]!.file, index)(!checked); - } - }; + }, + [annotatedFiles, selected, rangeStartIndex, handleSelect], + ); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -500,124 +495,146 @@ export const FileList: React.FC = ({ }, []); useEffect(() => { - if (selected.count === 0) { - setRangeStart(null); - } + if (selected.count == 0) setRangeStartIndex(undefined); }, [selected]); - const getThumbnail = ( - { file }: FileListAnnotatedFile, - index: number, - isScrolling: boolean, - ) => ( - onItemClick(index)} - selectable={selectable!} - onSelect={handleSelect(file, index)} - selected={ - (!mode - ? selected.collectionID === activeCollectionID - : mode == selected.context?.mode && - (selected.context.mode == "people" - ? selected.context.personID == activePersonID - : selected.context.collectionID == - activeCollectionID)) && !!selected[file.id] - } - selectOnClick={selected.count > 0} - onHover={onHoverOver(index)} - onRangeSelect={handleRangeSelect(index)} - isRangeSelectActive={isShiftKeyPressed && selected.count > 0} - isInsSelectRange={ - (index >= rangeStart! && index <= currentHover!) || - (index >= currentHover! && index <= rangeStart!) - } - activeCollectionID={activeCollectionID} - showPlaceholder={isScrolling} - isFav={favoriteFileIDs?.has(file.id)} - /> - ); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const renderListItem = ( - listItem: FileListItem, - isScrolling: boolean | undefined, - ) => { - const haveSelection = selected.count > 0; - switch (listItem.tag) { - case "date": - return listItem.dates ? ( - listItem.dates - .map((item) => [ - - {haveSelection && ( - - onChangeSelectAllCheckBox(item.date) - } - size="small" - sx={{ pl: 0 }} - /> - )} - {item.date} - , -
, - ]) - .flat() - ) : ( - - {haveSelection && ( - - onChangeSelectAllCheckBox(listItem.date!) + const renderListItem = useCallback( + ( + listItem: FileListItem, + layoutParams: ThumbnailGridLayoutParams, + isScrolling: boolean, + ) => { + const haveSelection = selected.count > 0; + switch (listItem.tag) { + case "date": + return listItem.dates ? ( + listItem.dates + .map((item) => [ + + {haveSelection && ( + + onChangeSelectAllCheckBox( + item.date, + ) + } + size="small" + sx={{ pl: 0 }} + /> + )} + {item.date} + , +
, + ]) + .flat() + ) : ( + + {haveSelection && ( + + onChangeSelectAllCheckBox( + listItem.date!, + ) + } + size="small" + sx={{ pl: 0 }} + /> + )} + {listItem.date} + + ); + case "file": { + const ret = listItem.items!.map(({ file }, i) => { + const index = listItem.itemStartIndex! + i; + return ( + 0} + isRangeSelectActive={ + isShiftKeyPressed && selected.count > 0 + } + isInSelectRange={ + rangeStartIndex !== undefined && + hoverIndex !== undefined && + ((index >= rangeStartIndex && + index <= hoverIndex) || + (index >= hoverIndex && + index <= rangeStartIndex)) + } + activeCollectionID={activeCollectionID} + showPlaceholder={isScrolling} + isFav={!!favoriteFileIDs?.has(file.id)} + onClick={() => onItemClick(index)} + onSelect={handleSelect(file, index)} + onHover={() => setHoverIndex(index)} + onRangeSelect={() => handleRangeSelect(index)} /> - )} - {listItem.date} - - ); - case "file": { - const ret = listItem.items!.map((item, idx) => - getThumbnail( - item, - listItem.itemStartIndex! + idx, - !!isScrolling, - ), - ); - if (listItem.groups) { - let sum = 0; - for (let i = 0; i < listItem.groups.length - 1; i++) { - sum = sum + listItem.groups[i]!; - ret.splice( - sum, - 0, -
, ); - sum += 1; + }); + if (listItem.groups) { + let sum = 0; + for (let i = 0; i < listItem.groups.length - 1; i++) { + sum = sum + listItem.groups[i]!; + ret.splice( + sum, + 0, +
, + ); + sum += 1; + } } + return ret; } - return ret; + default: + return listItem.component; } - default: - return listItem.component; - } - }; + }, + [ + activeCollectionID, + activePersonID, + checkedTimelineDateStrings, + emailByUserID, + favoriteFileIDs, + handleRangeSelect, + handleSelect, + hoverIndex, + isShiftKeyPressed, + mode, + onChangeSelectAllCheckBox, + onItemClick, + rangeStartIndex, + selectable, + selected, + user, + ], + ); const itemData = useMemo( () => ({ items, layoutParams, renderListItem }), @@ -681,9 +698,9 @@ export const FileList: React.FC = ({ }; /** - * Checks and merge multiple dates into a single row. + * Merge multiple dates into a single row. */ -const mergeTimeStampList = ( +const mergeRowsWherePossible = ( items: FileListItem[], columns: number, ): FileListItem[] => { @@ -814,9 +831,10 @@ interface FileListItemData { items: FileListItem[]; layoutParams: ThumbnailGridLayoutParams; renderListItem: ( - timeStampListItem: FileListItem, - isScrolling?: boolean, - ) => React.JSX.Element; + listItem: FileListItem, + layoutParams: ThumbnailGridLayoutParams, + isScrolling: boolean, + ) => React.ReactNode; } const FileListRow = memo( @@ -850,7 +868,7 @@ const FileListRow = memo( }, ]} > - {renderListItem(item, isScrolling)} + {renderListItem(item, layoutParams, !!isScrolling)} ); }, @@ -859,36 +877,36 @@ const FileListRow = memo( type FileThumbnailProps = { file: EnteFile; - onClick: () => void; selectable: boolean; selected: boolean; + isRangeSelectActive: boolean; + selectOnClick: boolean; + isInSelectRange: boolean; + activeCollectionID: number; + showPlaceholder: boolean; + isFav: boolean; + onClick: () => void; onSelect: (checked: boolean) => void; onHover: () => void; onRangeSelect: () => void; - isRangeSelectActive: boolean; - selectOnClick: boolean; - isInsSelectRange: boolean; - activeCollectionID: number; - showPlaceholder: boolean; - isFav: boolean | undefined; } & Pick; const FileThumbnail: React.FC = ({ file, user, - onClick, selectable, selected, - onSelect, selectOnClick, - onHover, - onRangeSelect, isRangeSelectActive, - isInsSelectRange, + isInSelectRange, isFav, emailByUserID, activeCollectionID, showPlaceholder, + onClick, + onSelect, + onHover, + onRangeSelect, }) => { const [imageURL, setImageURL] = useState(undefined); const [isLongPressing, setIsLongPressing] = useState(false); @@ -972,7 +990,7 @@ const FileThumbnail: React.FC = ({ type="checkbox" checked={selected} onChange={handleSelect} - $active={isRangeSelectActive && isInsSelectRange} + $active={isRangeSelectActive && isInSelectRange} onClick={(e) => e.stopPropagation()} /> )} @@ -1008,9 +1026,7 @@ const FileThumbnail: React.FC = ({ className="preview-card-hover-overlay" checked={selected} /> - {isRangeSelectActive && isInsSelectRange && ( - - )} + {isRangeSelectActive && isInSelectRange && } {deleteBy && ( diff --git a/web/apps/photos/src/utils/photoFrame/index.ts b/web/apps/photos/src/utils/photoFrame/index.ts index 5680bd0309..672c85d2b7 100644 --- a/web/apps/photos/src/utils/photoFrame/index.ts +++ b/web/apps/photos/src/utils/photoFrame/index.ts @@ -11,18 +11,15 @@ export const handleSelectCreator = userID: number | undefined, activeCollectionID: number, activePersonID: string | undefined, - // @ts-expect-error Need to add types - setRangeStart?, + setRangeStartIndex: (index: number | undefined) => void, ) => ({ id, ownerID }: { id: number; ownerID: number }, index?: number) => (checked: boolean) => { if (typeof index != "undefined") { if (checked) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - setRangeStart(index); + setRangeStartIndex(index); } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - setRangeStart(undefined); + setRangeStartIndex(undefined); } } setSelected((_selected) => {