This commit is contained in:
Manav Rathi
2025-07-16 16:32:50 +05:30
parent a97658b67d
commit 08a43f5d64
4 changed files with 220 additions and 243 deletions

View File

@@ -136,7 +136,7 @@ export const GalleryBarAndListHeader: React.FC<
if (shouldHide) return;
setFileListHeader({
item:
component:
mode != "people" ? (
<CollectionHeader
{...{

View File

@@ -31,9 +31,9 @@ import {
} from "ente-new/photos/components/utils/thumbnail-grid-layout";
import { PseudoCollectionID } from "ente-new/photos/services/collection-summary";
import { t } from "i18next";
import memoize from "memoize-one";
import React, {
memo,
useCallback,
useDeferredValue,
useEffect,
useMemo,
@@ -59,7 +59,7 @@ export interface FileListHeaderOrFooter {
/**
* The component itself.
*/
item: React.ReactNode;
component: React.ReactNode;
/**
* The height of the component (in px).
*/
@@ -70,7 +70,11 @@ export interface FileListHeaderOrFooter {
* Data needed to render each row in the variable size list that comprises the
* file list.
*/
interface FileListRow {
interface FileListItem {
/**
* The height of the row that will render this item.
*/
height: number;
/**
* An optional tag that can be used to identify item types for conditional
* behaviour.
@@ -81,12 +85,10 @@ interface FileListRow {
date?: string | null;
dates?: { date: string; span: number }[];
groups?: number[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
item?: any;
id?: string;
height?: number;
fileSize?: number;
fileCount?: number;
/**
* The React component that is the rendered representation of the item.
*/
component?: React.ReactNode;
}
export interface FileListAnnotatedFile {
@@ -218,8 +220,8 @@ export const FileList: React.FC<FileListProps> = ({
emailByUserID,
onItemClick,
}) => {
const [_rows, setRows] = useState<FileListRow[]>([]);
const rows = useDeferredValue(_rows);
const [_items, setItems] = useState<FileListItem[]>([]);
const items = useDeferredValue(_items);
const listRef = useRef<VariableSizeList | null>(null);
@@ -247,8 +249,6 @@ export const FileList: React.FC<FileListProps> = ({
// itemHeight,
// gap,
} = layoutParams;
// TODO(RE):
const listItemHeight = layoutParams.itemHeight + layoutParams.gap;
useEffect(() => {
// Since width and height are dependencies, there might be too many
@@ -259,59 +259,98 @@ export const FileList: React.FC<FileListProps> = ({
// another update when processing one, React will restart the background
// rerender from scratch.
let rows: FileListRow[] = [];
let items: FileListItem[] = [];
if (header) rows.push(asFullSpanListItem(header));
if (header) items.push(asFullSpanListItem(header));
const { isSmallerLayout, columns } = layoutParams;
const fileItemHeight = layoutParams.itemHeight + layoutParams.gap;
if (disableGrouping) {
let listItemIndex = columns;
annotatedFiles.forEach((item, index) => {
for (const [index, af] of annotatedFiles.entries()) {
if (listItemIndex < columns) {
rows[rows.length - 1]!.items!.push(item);
items[items.length - 1]!.items!.push(af);
listItemIndex++;
} else {
listItemIndex = 1;
rows.push({
items.push({
height: fileItemHeight,
tag: "file",
items: [item],
items: [af],
itemStartIndex: index,
});
}
});
}
} else {
groupByTime(rows);
let listItemIndex = 0;
let lastCreationTime: number | undefined;
for (const [index, af] of annotatedFiles.entries()) {
const creationTime = fileCreationTime(af.file) / 1000;
if (
!lastCreationTime ||
!isSameDay(
new Date(creationTime),
new Date(lastCreationTime),
)
) {
lastCreationTime = creationTime;
items.push({
height: dateContainerHeight,
tag: "date",
date: af.timelineDateString,
});
items.push({
height: fileItemHeight,
tag: "file",
items: [af],
itemStartIndex: index,
});
listItemIndex = 1;
} else if (listItemIndex < columns) {
items[items.length - 1]!.items!.push(af);
listItemIndex++;
} else {
listItemIndex = 1;
items.push({
height: fileItemHeight,
tag: "file",
items: [af],
itemStartIndex: index,
});
}
}
}
if (!isSmallerLayout) {
rows = mergeTimeStampList(rows, columns);
items = mergeTimeStampList(items, columns);
}
if (rows.length == 1) {
rows.push({
item: (
if (items.length == 1) {
items.push({
height: height - 48,
component: (
<NoFilesContainer span={columns}>
<Typography sx={{ color: "text.faint" }}>
{t("nothing_here")}
</Typography>
</NoFilesContainer>
),
id: "empty-list-banner",
height: height - 48,
});
}
const footerHeight = footer?.height ?? 0;
rows.push(getVacuumItem(rows, footerHeight));
if (footer) {
rows.push(asFullSpanListItem(footer));
let leftoverHeight = height - (footer?.height ?? 0);
for (const item of items) {
leftoverHeight -= item.height;
if (leftoverHeight <= 0) break;
}
if (leftoverHeight > 0) {
items.push({ height: leftoverHeight, component: <></> });
}
setRows(rows);
// Refresh list.
listRef.current?.resetAfterIndex(0);
// TODO:
// eslint-disable-next-line react-hooks/exhaustive-deps
if (footer) items.push(asFullSpanListItem(footer));
setItems(items);
}, [
width,
height,
@@ -322,181 +361,32 @@ export const FileList: React.FC<FileListProps> = ({
layoutParams,
]);
useEffect(() => {
// Refresh list
listRef.current?.resetAfterIndex(0);
}, [items]);
// TODO: Too many non-null assertions
const groupByTime = (timeStampList: FileListRow[]) => {
let listItemIndex = 0;
let lastCreationTime: number | undefined;
annotatedFiles.forEach((item, index) => {
const creationTime = fileCreationTime(item.file) / 1000;
if (
!lastCreationTime ||
!isSameDay(new Date(creationTime), new Date(lastCreationTime))
) {
lastCreationTime = creationTime;
timeStampList.push({
tag: "date",
date: item.timelineDateString,
id: lastCreationTime.toString(),
});
timeStampList.push({
tag: "file",
items: [item],
itemStartIndex: index,
});
listItemIndex = 1;
} else if (listItemIndex < columns) {
timeStampList[timeStampList.length - 1]!.items!.push(item);
listItemIndex++;
} else {
listItemIndex = 1;
timeStampList.push({
tag: "file",
items: [item],
itemStartIndex: index,
});
}
});
};
const getVacuumItem = (rows: FileListRow[], footerHeight: number) => {
const fileListHeight = (() => {
let sum = 0;
const getCurrentItemSize = getItemSize(rows);
for (let i = 0; i < rows.length; i++) {
sum += getCurrentItemSize(i);
if (height - sum <= footerHeight) {
break;
}
}
return sum;
})();
return {
item: <></>,
height: Math.max(height - fileListHeight - footerHeight, 0),
};
};
/**
* Checks and merge multiple dates into a single row.
*/
const mergeTimeStampList = (
items: FileListRow[],
columns: number,
): FileListRow[] => {
const newList: FileListRow[] = [];
let index = 0;
let newIndex = 0;
while (index < items.length) {
const currItem = items[index]!;
// If the current item is of type time, then it is not part of an ongoing date.
// So, there is a possibility of merge.
if (currItem.tag == "date") {
// If new list pointer is not at the end of list then
// we can add more items to the same list.
if (newList[newIndex]) {
const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
// Check if items can be added to same list
if (
newList[newIndex + 1]!.items!.length +
items[index + 1]!.items!.length +
Math.ceil(
newList[newIndex]!.dates!.length *
SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO,
) <=
columns
) {
newList[newIndex]!.dates!.push({
date: currItem.date!,
span: items[index + 1]!.items!.length,
});
newList[newIndex + 1]!.items = [
...newList[newIndex + 1]!.items!,
...items[index + 1]!.items!,
];
index += 2;
} else {
// Adding items would exceed the number of columns.
// So, move new list pointer to the end. Hence, in next iteration,
// items will be added to a new list.
newIndex += 2;
}
} else {
// New list pointer was at the end of list so simply add new items to the list.
newList.push({
...currItem,
date: null,
dates: [
{
date: currItem.date!,
span: items[index + 1]!.items!.length,
},
],
});
newList.push(items[index + 1]!);
index += 2;
}
} else {
// Merge cannot happen. Simply add all items to new list
// and set new list point to the end of list.
newList.push(currItem);
index++;
newIndex = newList.length;
}
}
for (let i = 0; i < newList.length; i++) {
const currItem = newList[i]!;
const nextItem = newList[i + 1]!;
if (currItem.tag == "date") {
if (currItem.dates!.length > 1) {
currItem.groups = currItem.dates!.map((item) => item.span);
nextItem.groups = currItem.groups;
}
}
}
return newList;
};
const getItemSize = (timeStampList: FileListRow[]) => (index: number) => {
switch (timeStampList[index]!.tag) {
case "date":
return dateContainerHeight;
case "file":
return listItemHeight;
default:
return timeStampList[index]!.height!;
}
};
const generateKey = (index: number) => {
switch (rows[index]!.tag) {
case "file":
return `${rows[index]!.items![0]!.file.id}-${
rows[index]!.items!.slice(-1)[0]!.file.id
}`;
default:
return `${rows[index]!.id}-${index}`;
}
};
useEffect(() => {
const notSelectedFiles = annotatedFiles.filter(
(item) => !selected[item.file.id],
(af) => !selected[af.file.id],
);
// Get dates of files which were manually unselected.
const unselectedDates = new Set(
notSelectedFiles.map((item) => item.timelineDateString),
); // to get file's date which were manually unselected
const localSelectedFiles = annotatedFiles.filter(
// to get files which were manually selected
(item) => !unselectedDates.has(item.timelineDateString),
notSelectedFiles.map((af) => af.timelineDateString),
);
// Get files which were manually selected.
const localSelectedFiles = annotatedFiles.filter(
(af) => !unselectedDates.has(af.timelineDateString),
);
// Get dates of files which were manually selected.
const localSelectedDates = new Set(
localSelectedFiles.map((item) => item.timelineDateString),
); // to get file's date which were manually selected
localSelectedFiles.map((af) => af.timelineDateString),
);
setCheckedTimelineDateStrings((prev) => {
const checked = new Set(prev);
@@ -508,9 +398,7 @@ export const FileList: React.FC<FileListProps> = ({
localSelectedDates.forEach((date) => checked.add(date));
return checked;
});
// TODO:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected]);
}, [annotatedFiles, selected]);
const handleSelectMulti = handleSelectCreatorMulti(
setSelected,
@@ -642,8 +530,9 @@ export const FileList: React.FC<FileListProps> = ({
/>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const renderListItem = (
listItem: FileListRow,
listItem: FileListItem,
isScrolling: boolean | undefined,
) => {
const haveSelection = selected.count > 0;
@@ -716,18 +605,38 @@ export const FileList: React.FC<FileListProps> = ({
return ret;
}
default:
// TODO:
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return listItem.item;
return listItem.component;
}
};
if (!rows.length) {
const itemData = useMemo(
() => ({ items, layoutParams, renderListItem }),
[items, layoutParams, renderListItem],
);
const itemSize = useCallback(
(index: number) => itemData.items[index]!.height,
[itemData],
);
const itemKey = useCallback((index: number, itemData: FileListItemData) => {
const item = itemData.items[index]!;
switch (item.tag) {
case "date":
return `${item.date ?? ""}-${index}`;
case "file":
return `${item.items![0]!.file.id}-${
item.items!.slice(-1)[0]!.file.id
}`;
default:
return `${index}`;
}
}, []);
if (!items.length) {
return <></>;
}
const itemData = createItemData(rows, layoutParams, renderListItem);
// The old, mode unaware, behaviour.
let key = `${activeCollectionID}`;
if (modePlus) {
@@ -747,21 +656,97 @@ export const FileList: React.FC<FileListProps> = ({
return (
<VariableSizeList
key={key}
itemData={itemData}
ref={listRef}
itemSize={getItemSize(rows)}
height={height}
width={width}
itemCount={rows.length}
itemKey={generateKey}
{...{ width, height, itemData, itemSize, itemKey }}
itemCount={items.length}
overscanCount={3}
useIsScrolling
>
{FileListItem}
{FileListRow}
</VariableSizeList>
);
};
/**
* Checks and merge multiple dates into a single row.
*/
const mergeTimeStampList = (
items: FileListItem[],
columns: number,
): FileListItem[] => {
const newList: FileListItem[] = [];
let index = 0;
let newIndex = 0;
while (index < items.length) {
const currItem = items[index]!;
// If the current item is of type time, then it is not part of an ongoing date.
// So, there is a possibility of merge.
if (currItem.tag == "date") {
// If new list pointer is not at the end of list then
// we can add more items to the same list.
if (newList[newIndex]) {
const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
// Check if items can be added to same list
if (
newList[newIndex + 1]!.items!.length +
items[index + 1]!.items!.length +
Math.ceil(
newList[newIndex]!.dates!.length *
SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO,
) <=
columns
) {
newList[newIndex]!.dates!.push({
date: currItem.date!,
span: items[index + 1]!.items!.length,
});
newList[newIndex + 1]!.items = [
...newList[newIndex + 1]!.items!,
...items[index + 1]!.items!,
];
index += 2;
} else {
// Adding items would exceed the number of columns.
// So, move new list pointer to the end. Hence, in next iteration,
// items will be added to a new list.
newIndex += 2;
}
} else {
// New list pointer was at the end of list so simply add new items to the list.
newList.push({
...currItem,
date: null,
dates: [
{
date: currItem.date!,
span: items[index + 1]!.items!.length,
},
],
});
newList.push(items[index + 1]!);
index += 2;
}
} else {
// Merge cannot happen. Simply add all items to new list
// and set new list point to the end of list.
newList.push(currItem);
index++;
newIndex = newList.length;
}
}
for (let i = 0; i < newList.length; i++) {
const currItem = newList[i]!;
const nextItem = newList[i + 1]!;
if (currItem.tag == "date") {
if (currItem.dates!.length > 1) {
currItem.groups = currItem.dates!.map((item) => item.span);
nextItem.groups = currItem.groups;
}
}
}
return newList;
};
const ListItem = styled("div")`
display: flex;
justify-content: center;
@@ -807,12 +792,14 @@ const FullSpanListItemContainer = styled("div")`
`;
/**
* Convert a {@link FileListHeaderOrFooter} into a {@link FileListRow}
* Convert a {@link FileListHeaderOrFooter} into a {@link FileListItem}
* that spans all columns.
*/
const asFullSpanListItem = ({ item, ...rest }: FileListHeaderOrFooter) => ({
...rest,
item: <FullSpanListItemContainer>{item}</FullSpanListItemContainer>,
const asFullSpanListItem = ({ component, height }: FileListHeaderOrFooter) => ({
height,
component: (
<FullSpanListItemContainer>{component}</FullSpanListItemContainer>
),
});
/**
@@ -836,26 +823,15 @@ const NoFilesContainer = styled(ListItemContainer)`
`;
interface FileListItemData {
items: FileListRow[];
items: FileListItem[];
layoutParams: ThumbnailGridLayoutParams;
renderListItem: (
timeStampListItem: FileListRow,
timeStampListItem: FileListItem,
isScrolling?: boolean,
) => React.JSX.Element;
}
const createItemData = memoize(
(
rowDatas: FileListRow[],
layoutParams: ThumbnailGridLayoutParams,
renderListItem: (
timeStampListItem: FileListRow,
isScrolling?: boolean,
) => React.JSX.Element,
): FileListItemData => ({ items: rowDatas, layoutParams, renderListItem }),
);
const FileListItem = memo(
const FileListRow = memo(
({
index,
style,

View File

@@ -412,7 +412,7 @@ const Page: React.FC = () => {
useEffect(() => {
if (isInSearchMode && state.searchSuggestion) {
setFileListHeader({
item: (
component: (
<SearchResultsHeader
searchSuggestion={state.searchSuggestion}
fileCount={state.searchResults?.length ?? 0}
@@ -1396,7 +1396,7 @@ const handleSubscriptionCompletionRedirectIfNeeded = async (
};
const createAppDownloadFooter = (): FileListHeaderOrFooter => ({
item: (
component: (
<Typography
variant="small"
sx={{

View File

@@ -15,6 +15,7 @@ import {
} from "@mui/material";
import Typography from "@mui/material/Typography";
import { DownloadStatusNotifications } from "components/DownloadStatusNotifications";
import { type FileListHeaderOrFooter } from "components/FileList";
import { FileListWithViewer } from "components/FileListWithViewer";
import { Upload } from "components/Upload";
import {
@@ -393,11 +394,11 @@ export default function PublicCollectionGallery() {
setUploadTypeSelectorView(false);
};
const fileListHeader = useMemo(
const fileListHeader = useMemo<FileListHeaderOrFooter | undefined>(
() =>
publicCollection && publicFiles
? {
item: (
component: (
<FileListHeader
{...{
publicCollection,
@@ -413,10 +414,10 @@ export default function PublicCollectionGallery() {
[onAddSaveGroup, publicCollection, publicFiles, downloadEnabled],
);
const fileListFooter = useMemo(() => {
const fileListFooter = useMemo<FileListHeaderOrFooter>(() => {
const props = { referralCode, onAddPhotos };
return {
item: <FileListFooter {...props} />,
component: <FileListFooter {...props} />,
height: fileListFooterHeightForProps(props),
};
}, [referralCode, onAddPhotos]);