[web] Enable strictNullChecks in photos tsconfig (#6492)
This commit is contained in:
@@ -9,17 +9,10 @@ export default [
|
||||
* "This rule requires the `strictNullChecks` compiler option to be
|
||||
* turned on to function correctly"
|
||||
*/
|
||||
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
|
||||
"@typescript-eslint/no-unnecessary-condition": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
/** TODO: Disabled as we migrate, try to prune these again */
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -87,6 +87,7 @@ export const AlbumCastDialogContents: React.FC<AlbumCastDialogProps> = ({
|
||||
// (effectively, only Chrome).
|
||||
//
|
||||
// Override, otherwise tsc complains about unknown property `chrome`.
|
||||
// @ts-expect-error TODO: why is this needed
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
setBrowserCanCast(typeof window["chrome"] != "undefined");
|
||||
}, []);
|
||||
@@ -127,7 +128,10 @@ export const AlbumCastDialogContents: React.FC<AlbumCastDialogProps> = ({
|
||||
"urn:x-cast:pair-request",
|
||||
(_, message) => {
|
||||
const data = message;
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const obj = JSON.parse(data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const code = obj.code;
|
||||
|
||||
if (code) {
|
||||
|
||||
@@ -101,7 +101,15 @@ const AllAlbumsDialog = styled(Dialog)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const Title = ({
|
||||
type TitleProps = { collectionCount: number } & Pick<
|
||||
AllAlbums,
|
||||
| "onClose"
|
||||
| "collectionsSortBy"
|
||||
| "onChangeCollectionsSortBy"
|
||||
| "isInHiddenSection"
|
||||
>;
|
||||
|
||||
const Title: React.FC<TitleProps> = ({
|
||||
onClose,
|
||||
collectionCount,
|
||||
collectionsSortBy,
|
||||
@@ -154,7 +162,10 @@ interface ItemData {
|
||||
// If we were only passing a single, stable value (e.g. items),
|
||||
// We could just pass the value directly.
|
||||
const createItemData = memoize((collectionRowList, onCollectionClick) => ({
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
collectionRowList,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
onCollectionClick,
|
||||
}));
|
||||
|
||||
@@ -174,7 +185,7 @@ const AlbumsRow = React.memo(
|
||||
return (
|
||||
<div style={style}>
|
||||
<Stack direction="row" sx={{ p: 2, gap: 0.5 }}>
|
||||
{collectionRow.map((item: any) => (
|
||||
{collectionRow.map((item) => (
|
||||
<AlbumCard
|
||||
isScrolling={isScrolling}
|
||||
onCollectionClick={onCollectionClick}
|
||||
|
||||
@@ -128,7 +128,7 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
const group = saveGroups.find(
|
||||
(g) => g.collectionSummaryID === activeCollectionID,
|
||||
);
|
||||
return group && !isSaveComplete(group) && !isSaveCancelled(group);
|
||||
return !!group && !isSaveComplete(group) && !isSaveCancelled(group);
|
||||
}, [saveGroups, activeCollectionID]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,9 +145,9 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
onRemotePull,
|
||||
onAddSaveGroup,
|
||||
}}
|
||||
collectionSummary={toShowCollectionSummaries.get(
|
||||
activeCollectionID,
|
||||
)}
|
||||
collectionSummary={
|
||||
toShowCollectionSummaries.get(activeCollectionID!)!
|
||||
}
|
||||
onCollectionShare={showCollectionShare}
|
||||
onCollectionCast={showCollectionCast}
|
||||
/>
|
||||
@@ -170,7 +170,7 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
activePerson,
|
||||
showCollectionShare,
|
||||
showCollectionCast,
|
||||
// TODO-Cluster
|
||||
// TODO: Cluster
|
||||
// This causes a loop since it is an array dep
|
||||
// people,
|
||||
]);
|
||||
@@ -211,9 +211,9 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
/>
|
||||
<CollectionShare
|
||||
{...collectionShareVisibilityProps}
|
||||
collectionSummary={toShowCollectionSummaries.get(
|
||||
activeCollectionID,
|
||||
)}
|
||||
collectionSummary={
|
||||
toShowCollectionSummaries.get(activeCollectionID!)!
|
||||
}
|
||||
collection={activeCollection}
|
||||
{...{
|
||||
user,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// TODO: Audit this file
|
||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
|
||||
import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded";
|
||||
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
|
||||
@@ -30,10 +29,16 @@ import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles";
|
||||
import { PseudoCollectionID } from "ente-new/photos/services/collection-summary";
|
||||
import { t } from "i18next";
|
||||
import memoize from "memoize-one";
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
VariableSizeList as List,
|
||||
type ListChildComponentProps,
|
||||
VariableSizeList,
|
||||
areEqual,
|
||||
} from "react-window";
|
||||
import { type SelectedState, shouldShowAvatar } from "utils/file";
|
||||
@@ -41,13 +46,9 @@ import {
|
||||
handleSelectCreator,
|
||||
handleSelectCreatorMulti,
|
||||
} from "utils/photoFrame";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
|
||||
export const DATE_CONTAINER_HEIGHT = 48;
|
||||
export const SPACE_BTW_DATES = 44;
|
||||
|
||||
const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
|
||||
|
||||
/**
|
||||
* A component with an explicit height suitable for being plugged in as the
|
||||
* {@link header} or {@link footer} of the {@link FileList}.
|
||||
@@ -71,7 +72,7 @@ interface TimeStampListItem {
|
||||
tag?: "date" | "file";
|
||||
items?: FileListAnnotatedFile[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
date?: string | null;
|
||||
dates?: { date: string; span: number }[];
|
||||
groups?: number[];
|
||||
item?: any;
|
||||
@@ -210,14 +211,12 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
emailByUserID,
|
||||
onItemClick,
|
||||
}) => {
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext,
|
||||
const [_timeStampList, setTimeStampList] = useState(
|
||||
new Array<TimeStampListItem>(),
|
||||
);
|
||||
const timeStampList = useDeferredValue(_timeStampList);
|
||||
|
||||
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||
const refreshInProgress = useRef(false);
|
||||
const shouldRefresh = useRef(false);
|
||||
const listRef = useRef(null);
|
||||
const listRef = useRef<VariableSizeList | null>(null);
|
||||
|
||||
// Timeline date strings for which all photos have been selected.
|
||||
//
|
||||
@@ -225,8 +224,8 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
const [checkedTimelineDateStrings, setCheckedTimelineDateStrings] =
|
||||
useState(new Set());
|
||||
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [rangeStart, setRangeStart] = useState<number | null>(null);
|
||||
const [currentHover, setCurrentHover] = useState<number | null>(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
|
||||
const fittableColumns = getFractionFittableColumns(width);
|
||||
@@ -237,66 +236,74 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
columns = MIN_COLUMNS;
|
||||
skipMerge = true;
|
||||
}
|
||||
|
||||
const shrinkRatio = getShrinkRatio(width, columns);
|
||||
const listItemHeight =
|
||||
IMAGE_CONTAINER_MAX_HEIGHT * shrinkRatio + GAP_BTW_TILES;
|
||||
|
||||
const refreshList = () => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const main = () => {
|
||||
if (refreshInProgress.current) {
|
||||
shouldRefresh.current = true;
|
||||
return;
|
||||
}
|
||||
refreshInProgress.current = true;
|
||||
let timeStampList: TimeStampListItem[] = [];
|
||||
// Since width and height are dependencies, there might be too many
|
||||
// updates to the list during a resize. The list computation too, while
|
||||
// fast, is non-trivial.
|
||||
//
|
||||
// To avoid these issues, the we use `useDeferredValue`: if it gets
|
||||
// another update when processing one, React will restart the background
|
||||
// rerender from scratch.
|
||||
|
||||
if (header) {
|
||||
timeStampList.push(asFullSpanListItem(header));
|
||||
}
|
||||
if (disableGrouping) {
|
||||
noGrouping(timeStampList);
|
||||
} else {
|
||||
groupByTime(timeStampList);
|
||||
}
|
||||
let timeStampList: TimeStampListItem[] = [];
|
||||
|
||||
if (!skipMerge) {
|
||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
||||
}
|
||||
if (timeStampList.length === 1) {
|
||||
timeStampList.push(getEmptyListItem());
|
||||
}
|
||||
const footerHeight = footer?.height ?? 0;
|
||||
timeStampList.push(getVacuumItem(timeStampList, footerHeight));
|
||||
if (footer) {
|
||||
timeStampList.push(asFullSpanListItem(footer));
|
||||
}
|
||||
if (header) {
|
||||
timeStampList.push(asFullSpanListItem(header));
|
||||
}
|
||||
|
||||
setTimeStampList(timeStampList);
|
||||
refreshInProgress.current = false;
|
||||
if (shouldRefresh.current) {
|
||||
shouldRefresh.current = false;
|
||||
setTimeout(main, 0);
|
||||
}
|
||||
};
|
||||
main();
|
||||
if (disableGrouping) {
|
||||
noGrouping(timeStampList);
|
||||
} else {
|
||||
groupByTime(timeStampList);
|
||||
}
|
||||
|
||||
if (!skipMerge) {
|
||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
||||
}
|
||||
|
||||
if (timeStampList.length == 1) {
|
||||
timeStampList.push({
|
||||
item: (
|
||||
<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;
|
||||
timeStampList.push(getVacuumItem(timeStampList, footerHeight));
|
||||
if (footer) {
|
||||
timeStampList.push(asFullSpanListItem(footer));
|
||||
}
|
||||
|
||||
setTimeStampList(timeStampList);
|
||||
}, [
|
||||
width,
|
||||
height,
|
||||
annotatedFiles,
|
||||
header,
|
||||
footer,
|
||||
annotatedFiles,
|
||||
disableGrouping,
|
||||
publicCollectionGalleryContext.credentials,
|
||||
columns,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshList();
|
||||
// Refresh list.
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
}, [timeStampList]);
|
||||
|
||||
// TODO: Too many non-null assertions
|
||||
|
||||
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
||||
let listItemIndex = 0;
|
||||
let lastCreationTime: number | undefined;
|
||||
@@ -320,7 +327,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
});
|
||||
listItemIndex = 1;
|
||||
} else if (listItemIndex < columns) {
|
||||
timeStampList[timeStampList.length - 1].items.push(item);
|
||||
timeStampList[timeStampList.length - 1]!.items!.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
@@ -337,7 +344,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
let listItemIndex = columns;
|
||||
annotatedFiles.forEach((item, index) => {
|
||||
if (listItemIndex < columns) {
|
||||
timeStampList[timeStampList.length - 1].items.push(item);
|
||||
timeStampList[timeStampList.length - 1]!.items!.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
@@ -350,21 +357,10 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getEmptyListItem = () => {
|
||||
return {
|
||||
item: (
|
||||
<NothingContainer span={columns}>
|
||||
<Typography sx={{ color: "text.faint" }}>
|
||||
{t("nothing_here")}
|
||||
</Typography>
|
||||
</NothingContainer>
|
||||
),
|
||||
id: "empty-list-banner",
|
||||
height: height - 48,
|
||||
};
|
||||
};
|
||||
|
||||
const getVacuumItem = (timeStampList, footerHeight: number) => {
|
||||
const getVacuumItem = (
|
||||
timeStampList: TimeStampListItem[],
|
||||
footerHeight: number,
|
||||
) => {
|
||||
const fileListHeight = (() => {
|
||||
let sum = 0;
|
||||
const getCurrentItemSize = getItemSize(timeStampList);
|
||||
@@ -393,30 +389,31 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
let index = 0;
|
||||
let newIndex = 0;
|
||||
while (index < items.length) {
|
||||
const currItem = items[index];
|
||||
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 +
|
||||
newList[newIndex + 1]!.items!.length +
|
||||
items[index + 1]!.items!.length +
|
||||
Math.ceil(
|
||||
newList[newIndex].dates.length *
|
||||
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]!.dates!.push({
|
||||
date: currItem.date!,
|
||||
span: items[index + 1]!.items!.length,
|
||||
});
|
||||
newList[newIndex + 1].items = [
|
||||
...newList[newIndex + 1].items,
|
||||
...items[index + 1].items,
|
||||
newList[newIndex + 1]!.items = [
|
||||
...newList[newIndex + 1]!.items!,
|
||||
...items[index + 1]!.items!,
|
||||
];
|
||||
index += 2;
|
||||
} else {
|
||||
@@ -432,12 +429,12 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
date: null,
|
||||
dates: [
|
||||
{
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
date: currItem.date!,
|
||||
span: items[index + 1]!.items!.length,
|
||||
},
|
||||
],
|
||||
});
|
||||
newList.push(items[index + 1]);
|
||||
newList.push(items[index + 1]!);
|
||||
index += 2;
|
||||
}
|
||||
} else {
|
||||
@@ -449,11 +446,11 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
const currItem = newList[i];
|
||||
const nextItem = newList[i + 1];
|
||||
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);
|
||||
if (currItem.dates!.length > 1) {
|
||||
currItem.groups = currItem.dates!.map((item) => item.span);
|
||||
nextItem.groups = currItem.groups;
|
||||
}
|
||||
}
|
||||
@@ -461,25 +458,26 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
return newList;
|
||||
};
|
||||
|
||||
const getItemSize = (timeStampList) => (index) => {
|
||||
switch (timeStampList[index].tag) {
|
||||
case "date":
|
||||
return DATE_CONTAINER_HEIGHT;
|
||||
case "file":
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index].height;
|
||||
}
|
||||
};
|
||||
const getItemSize =
|
||||
(timeStampList: TimeStampListItem[]) => (index: number) => {
|
||||
switch (timeStampList[index]!.tag) {
|
||||
case "date":
|
||||
return dateContainerHeight;
|
||||
case "file":
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index]!.height!;
|
||||
}
|
||||
};
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].tag) {
|
||||
const generateKey = (index: number) => {
|
||||
switch (timeStampList[index]!.tag) {
|
||||
case "file":
|
||||
return `${timeStampList[index].items[0].file.id}-${
|
||||
timeStampList[index].items.slice(-1)[0].file.id
|
||||
return `${timeStampList[index]!.items![0]!.file.id}-${
|
||||
timeStampList[index]!.items!.slice(-1)[0]!.file.id
|
||||
}`;
|
||||
default:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
return `${timeStampList[index]!.id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -563,23 +561,23 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
const handleRangeSelect = (index: number) => () => {
|
||||
if (typeof rangeStart != "undefined" && rangeStart !== index) {
|
||||
const direction =
|
||||
(index - rangeStart) / Math.abs(index - rangeStart);
|
||||
(index - rangeStart!) / Math.abs(index - rangeStart!);
|
||||
let checked = true;
|
||||
for (
|
||||
let i = rangeStart;
|
||||
let i = rangeStart!;
|
||||
(index - i) * direction >= 0;
|
||||
i += direction
|
||||
) {
|
||||
checked = checked && !!selected[annotatedFiles[i].file.id];
|
||||
checked = checked && !!selected[annotatedFiles[i]!.file.id];
|
||||
}
|
||||
for (
|
||||
let i = rangeStart;
|
||||
let i = rangeStart!;
|
||||
(index - i) * direction > 0;
|
||||
i += direction
|
||||
) {
|
||||
handleSelect(annotatedFiles[i].file)(!checked);
|
||||
handleSelect(annotatedFiles[i]!.file)(!checked);
|
||||
}
|
||||
handleSelect(annotatedFiles[index].file, index)(!checked);
|
||||
handleSelect(annotatedFiles[index]!.file, index)(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -621,7 +619,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
{...{ user, emailByUserID }}
|
||||
file={file}
|
||||
onClick={() => onItemClick(index)}
|
||||
selectable={selectable}
|
||||
selectable={selectable!}
|
||||
onSelect={handleSelect(file, index)}
|
||||
selected={
|
||||
(!mode
|
||||
@@ -637,8 +635,8 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart)
|
||||
(index >= rangeStart! && index <= currentHover!) ||
|
||||
(index >= currentHover! && index <= rangeStart!)
|
||||
}
|
||||
activeCollectionID={activeCollectionID}
|
||||
showPlaceholder={isScrolling}
|
||||
@@ -648,7 +646,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem,
|
||||
isScrolling: boolean,
|
||||
isScrolling: boolean | undefined,
|
||||
) => {
|
||||
const haveSelection = (selected.count ?? 0) > 0;
|
||||
switch (listItem.tag) {
|
||||
@@ -681,12 +679,12 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
{haveSelection && (
|
||||
<Checkbox
|
||||
key={listItem.date}
|
||||
name={listItem.date}
|
||||
name={listItem.date!}
|
||||
checked={checkedTimelineDateStrings.has(
|
||||
listItem.date,
|
||||
)}
|
||||
onChange={() =>
|
||||
onChangeSelectAllCheckBox(listItem.date)
|
||||
onChangeSelectAllCheckBox(listItem.date!)
|
||||
}
|
||||
size="small"
|
||||
sx={{ pl: 0 }}
|
||||
@@ -696,22 +694,22 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
</DateContainer>
|
||||
);
|
||||
case "file": {
|
||||
const ret = listItem.items.map((item, idx) =>
|
||||
const ret = listItem.items!.map((item, idx) =>
|
||||
getThumbnail(
|
||||
item,
|
||||
listItem.itemStartIndex + idx,
|
||||
isScrolling,
|
||||
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];
|
||||
sum = sum + listItem.groups[i]!;
|
||||
ret.splice(
|
||||
sum,
|
||||
0,
|
||||
<div
|
||||
key={`${listItem.items[0].file.id}-gap-${i}`}
|
||||
key={`${listItem.items![0]!.file.id}-gap-${i}`}
|
||||
/>,
|
||||
);
|
||||
sum += 1;
|
||||
@@ -720,6 +718,8 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
return ret;
|
||||
}
|
||||
default:
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return listItem.item;
|
||||
}
|
||||
};
|
||||
@@ -752,7 +752,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
<VariableSizeList
|
||||
key={key}
|
||||
itemData={itemData}
|
||||
ref={listRef}
|
||||
@@ -765,7 +765,7 @@ export const FileList: React.FC<FileListProps> = ({
|
||||
useIsScrolling
|
||||
>
|
||||
{PhotoListRow}
|
||||
</List>
|
||||
</VariableSizeList>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -835,34 +835,47 @@ const ListContainer = styled(Box, {
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* An grid item, spanning {@link span} columns.
|
||||
*/
|
||||
const ListItemContainer = styled("div")<{ span: number }>`
|
||||
grid-column: span ${(props) => props.span};
|
||||
grid-column: span ${({ span }) => span};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
/**
|
||||
* A grid items that spans all columns.
|
||||
*/
|
||||
const FullSpanListItemContainer = styled("div")`
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const asFullSpanListItem = ({ item, ...rest }: TimeStampListItem) => ({
|
||||
/**
|
||||
* Convert a {@link FileListHeaderOrFooter} into a {@link TimeStampListItem}
|
||||
* that spans all columns.
|
||||
*/
|
||||
const asFullSpanListItem = ({ item, ...rest }: FileListHeaderOrFooter) => ({
|
||||
...rest,
|
||||
item: <FullSpanListItemContainer>{item}</FullSpanListItemContainer>,
|
||||
});
|
||||
|
||||
const DateContainer = styled(ListItemContainer)(
|
||||
({ theme }) => `
|
||||
/**
|
||||
* The fixed height (in px) of {@link DateContainer}.
|
||||
*/
|
||||
const dateContainerHeight = 48;
|
||||
|
||||
const DateContainer = styled(ListItemContainer)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
color: ${theme.vars.palette.text.muted};
|
||||
`,
|
||||
);
|
||||
height: ${dateContainerHeight}px;
|
||||
color: "text.muted";
|
||||
`;
|
||||
|
||||
const NothingContainer = styled(ListItemContainer)`
|
||||
const NoFilesContainer = styled(ListItemContainer)`
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -903,10 +916,10 @@ const PhotoListRow = React.memo(
|
||||
gridTemplateColumns={getTemplateColumns(
|
||||
columns,
|
||||
shrinkRatio,
|
||||
timeStampList[index].groups,
|
||||
timeStampList[index]!.groups,
|
||||
)}
|
||||
>
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
{renderListItem(timeStampList[index]!, isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
@@ -927,7 +940,7 @@ type FileThumbnailProps = {
|
||||
isInsSelectRange: boolean;
|
||||
activeCollectionID: number;
|
||||
showPlaceholder: boolean;
|
||||
isFav: boolean;
|
||||
isFav: boolean | undefined;
|
||||
} & Pick<FileListProps, "user" | "emailByUserID">;
|
||||
|
||||
const FileThumbnail: React.FC<FileThumbnailProps> = ({
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "ente-base/components/utils/modal";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import { basename, dirname, joinPath } from "ente-base/file-name";
|
||||
import type { PublicAlbumsCredentials } from "ente-base/http";
|
||||
import log from "ente-base/log";
|
||||
import type { CollectionMapping, Electron, ZipItem } from "ente-base/types/ipc";
|
||||
import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload";
|
||||
@@ -68,13 +69,7 @@ import { redirectToCustomerPortal } from "ente-new/photos/services/user-details"
|
||||
import { usePhotosAppContext } from "ente-new/photos/types/context";
|
||||
import { firstNonEmpty } from "ente-utils/array";
|
||||
import { t } from "i18next";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type {
|
||||
InProgressUpload,
|
||||
SegregatedFinishedUploads,
|
||||
@@ -84,18 +79,24 @@ import type {
|
||||
} from "services/upload-manager";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import watcher from "services/watch";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
import { UploadProgress } from "./UploadProgress";
|
||||
|
||||
interface UploadProps {
|
||||
/**
|
||||
* The currently logged in user, if any.
|
||||
* The logged in user, if any.
|
||||
*
|
||||
* This is only expected to be present when we're running it the context of
|
||||
* the photos app, where there is a logged in user. When used by the public
|
||||
* albums app, this prop can be omitted.
|
||||
*/
|
||||
user?: LocalUser;
|
||||
/**
|
||||
* The {@link PublicAlbumsCredentials} to use, if any.
|
||||
*
|
||||
* These are expected to be set if we are in the context of the public
|
||||
* albums app, and should be undefined when we're in the photos app context.
|
||||
*/
|
||||
publicAlbumsCredentials?: PublicAlbumsCredentials;
|
||||
isFirstUpload?: boolean;
|
||||
uploadTypeSelectorView: boolean;
|
||||
dragAndDropFiles: File[];
|
||||
@@ -164,6 +165,7 @@ type UploadType = "files" | "folders" | "zips";
|
||||
*/
|
||||
export const Upload: React.FC<UploadProps> = ({
|
||||
user,
|
||||
publicAlbumsCredentials,
|
||||
isFirstUpload,
|
||||
dragAndDropFiles,
|
||||
onRemotePull,
|
||||
@@ -177,9 +179,6 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
}) => {
|
||||
const { showMiniDialog, onGenericError } = useBaseContext();
|
||||
const { showNotification, watchFolderView } = usePhotosAppContext();
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext,
|
||||
);
|
||||
|
||||
const [uploadProgressView, setUploadProgressView] = useState(false);
|
||||
const [uploadPhase, setUploadPhase] = useState<UploadPhase>("preparing");
|
||||
@@ -378,7 +377,7 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
setUploadProgressView,
|
||||
},
|
||||
onUploadFile,
|
||||
publicCollectionGalleryContext.credentials,
|
||||
publicAlbumsCredentials,
|
||||
);
|
||||
|
||||
if (uploadManager.isUploadRunning()) {
|
||||
@@ -408,7 +407,7 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
setDesktopZipItems(zipItems);
|
||||
});
|
||||
}
|
||||
}, [publicCollectionGalleryContext.credentials]);
|
||||
}, [publicAlbumsCredentials]);
|
||||
|
||||
// Handle selected files when user selects files for upload through the open
|
||||
// file / open folder selection dialog, or drag-and-drops them.
|
||||
@@ -527,10 +526,10 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
props.setLoading(false);
|
||||
|
||||
(async () => {
|
||||
if (publicCollectionGalleryContext.credentials) {
|
||||
if (publicAlbumsCredentials) {
|
||||
setUploaderName(
|
||||
(await savedPublicCollectionUploaderName(
|
||||
publicCollectionGalleryContext.credentials.accessToken,
|
||||
publicAlbumsCredentials.accessToken,
|
||||
)) ?? "",
|
||||
);
|
||||
showUploaderNameInput();
|
||||
@@ -591,7 +590,13 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
onCancel: handleCollectionSelectorCancel,
|
||||
});
|
||||
})();
|
||||
}, [webFiles, desktopFiles, desktopFilePaths, desktopZipItems]);
|
||||
}, [
|
||||
publicAlbumsCredentials,
|
||||
webFiles,
|
||||
desktopFiles,
|
||||
desktopFilePaths,
|
||||
desktopZipItems,
|
||||
]);
|
||||
|
||||
const preCollectionCreationAction = () => {
|
||||
onCloseCollectionSelector?.();
|
||||
@@ -832,7 +837,7 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
|
||||
const handlePublicUpload = (uploaderName: string) => {
|
||||
savePublicCollectionUploaderName(
|
||||
publicCollectionGalleryContext.credentials!.accessToken,
|
||||
publicAlbumsCredentials!.accessToken,
|
||||
uploaderName,
|
||||
);
|
||||
|
||||
@@ -868,6 +873,7 @@ export const Upload: React.FC<UploadProps> = ({
|
||||
<UploadTypeSelector
|
||||
open={props.uploadTypeSelectorView}
|
||||
onClose={props.closeUploadTypeSelector}
|
||||
publicAlbumsCredentials={publicAlbumsCredentials}
|
||||
intent={props.uploadTypeSelectorIntent}
|
||||
pendingUploadType={
|
||||
isInputPending ? selectedUploadType.current : undefined
|
||||
@@ -1115,7 +1121,7 @@ type UploadTypeSelectorProps = ModalVisibilityProps & {
|
||||
* Called when the user selects one of the options.
|
||||
*/
|
||||
onSelect: (type: UploadType) => void;
|
||||
};
|
||||
} & Pick<UploadProps, "publicAlbumsCredentials">;
|
||||
|
||||
/**
|
||||
* Request the user to specify which type of file / folder / zip it is that they
|
||||
@@ -1129,28 +1135,21 @@ type UploadTypeSelectorProps = ModalVisibilityProps & {
|
||||
const UploadTypeSelector: React.FC<UploadTypeSelectorProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
publicAlbumsCredentials,
|
||||
intent,
|
||||
pendingUploadType,
|
||||
onSelect,
|
||||
}) => {
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext,
|
||||
);
|
||||
|
||||
// Directly show the file selector for the public albums app on likely
|
||||
// mobile devices.
|
||||
const directlyShowUploadFiles = useIsTouchscreen();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
open &&
|
||||
directlyShowUploadFiles &&
|
||||
publicCollectionGalleryContext.credentials
|
||||
) {
|
||||
if (open && directlyShowUploadFiles && publicAlbumsCredentials) {
|
||||
onSelect("files");
|
||||
onClose();
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, publicAlbumsCredentials]);
|
||||
|
||||
const handleClose: DialogProps["onClose"] = (_, reason) => {
|
||||
// Disable backdrop clicks and esc keypresses if a selection is pending
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: Audit this file
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
|
||||
@@ -32,6 +34,7 @@ import { t } from "i18next";
|
||||
import memoize from "memoize-one";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -87,7 +90,7 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
|
||||
if (open) setExpanded(false);
|
||||
}, [open]);
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = useCallback(() => {
|
||||
if (uploadPhase == "done") {
|
||||
onClose();
|
||||
} else {
|
||||
@@ -102,7 +105,7 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
|
||||
cancel: t("no"),
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [uploadPhase, onClose, cancelUploads, showMiniDialog]);
|
||||
|
||||
if (!open) {
|
||||
return <></>;
|
||||
@@ -130,6 +133,9 @@ export const UploadProgress: React.FC<UploadProgressProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A context internal to the components of this file.
|
||||
*/
|
||||
interface UploadProgressContextT {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -145,20 +151,19 @@ interface UploadProgressContextT {
|
||||
setExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const UploadProgressContext = createContext<UploadProgressContextT>({
|
||||
open: null,
|
||||
onClose: () => null,
|
||||
uploadCounter: null,
|
||||
uploadPhase: undefined,
|
||||
percentComplete: null,
|
||||
retryFailed: () => null,
|
||||
inProgressUploads: null,
|
||||
uploadFileNames: null,
|
||||
finishedUploads: null,
|
||||
hasLivePhotos: null,
|
||||
expanded: null,
|
||||
setExpanded: () => null,
|
||||
});
|
||||
const UploadProgressContext = createContext<UploadProgressContextT | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* Convenience hook to obtain the non-null asserted
|
||||
* {@link UploadProgressContext}.
|
||||
*
|
||||
* The non-null assertion is reasonable since we provide it to the tree always
|
||||
* in an invariant that is local to this file (and thus has less chance of being
|
||||
* invalid in the future).
|
||||
*/
|
||||
const useUploadProgressContext = () => useContext(UploadProgressContext)!;
|
||||
|
||||
const MinimizedUploadProgress: React.FC = () => (
|
||||
<Snackbar open anchorOrigin={{ horizontal: "right", vertical: "bottom" }}>
|
||||
@@ -176,9 +181,7 @@ const UploadProgressHeader: React.FC = () => (
|
||||
);
|
||||
|
||||
const UploadProgressTitle: React.FC = () => {
|
||||
const { setExpanded, onClose, expanded } = useContext(
|
||||
UploadProgressContext,
|
||||
);
|
||||
const { setExpanded, onClose, expanded } = useUploadProgressContext();
|
||||
const toggleExpanded = () => setExpanded((expanded) => !expanded);
|
||||
|
||||
return (
|
||||
@@ -202,9 +205,8 @@ const UploadProgressTitle: React.FC = () => {
|
||||
};
|
||||
|
||||
const UploadProgressSubtitleText: React.FC = () => {
|
||||
const { uploadPhase, uploadCounter, finishedUploads } = useContext(
|
||||
UploadProgressContext,
|
||||
);
|
||||
const { uploadPhase, uploadCounter, finishedUploads } =
|
||||
useUploadProgressContext();
|
||||
|
||||
return (
|
||||
<Typography
|
||||
@@ -280,7 +282,7 @@ const notUploadedFileCount = (
|
||||
};
|
||||
|
||||
const UploadProgressBar: React.FC = () => {
|
||||
const { uploadPhase, percentComplete } = useContext(UploadProgressContext);
|
||||
const { uploadPhase, percentComplete } = useUploadProgressContext();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@@ -300,9 +302,8 @@ const UploadProgressBar: React.FC = () => {
|
||||
};
|
||||
|
||||
function UploadProgressDialog() {
|
||||
const { open, onClose, uploadPhase, finishedUploads } = useContext(
|
||||
UploadProgressContext,
|
||||
);
|
||||
const { open, onClose, uploadPhase, finishedUploads } =
|
||||
useUploadProgressContext();
|
||||
|
||||
const [hasUnUploadedFiles, setHasUnUploadedFiles] = useState(false);
|
||||
|
||||
@@ -377,11 +378,14 @@ function UploadProgressDialog() {
|
||||
|
||||
const InProgressSection: React.FC = () => {
|
||||
const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadPhase } =
|
||||
useContext(UploadProgressContext);
|
||||
useUploadProgressContext();
|
||||
|
||||
const fileList = inProgressUploads ?? [];
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const renderListItem = ({ localFileID, progress }) => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
<InProgressItemContainer key={localFileID}>
|
||||
<span>{uploadFileNames.get(localFileID)}</span>
|
||||
{uploadPhase == "uploading" && (
|
||||
@@ -395,10 +399,12 @@ const InProgressSection: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const getItemTitle = ({ localFileID, progress }) => {
|
||||
return `${uploadFileNames.get(localFileID)} - ${progress}%`;
|
||||
};
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const generateItemKey = ({ localFileID, progress }) => {
|
||||
return `${localFileID}-${progress}`;
|
||||
};
|
||||
@@ -492,28 +498,32 @@ const ResultSection: React.FC<ResultSectionProps> = ({
|
||||
sectionTitle,
|
||||
sectionInfo,
|
||||
}) => {
|
||||
const { finishedUploads, uploadFileNames } = useContext(
|
||||
UploadProgressContext,
|
||||
);
|
||||
const { finishedUploads, uploadFileNames } = useUploadProgressContext();
|
||||
|
||||
const fileList = finishedUploads.get(resultType);
|
||||
|
||||
if (!fileList?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const renderListItem = (fileID) => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
<ResultItemContainer key={fileID}>
|
||||
{uploadFileNames.get(fileID)}
|
||||
</ResultItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const getItemTitle = (fileID) => {
|
||||
return uploadFileNames.get(fileID);
|
||||
return uploadFileNames.get(fileID)!;
|
||||
};
|
||||
|
||||
// @ts-expect-error Need to add types
|
||||
const generateItemKey = (fileID) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return fileID;
|
||||
};
|
||||
|
||||
@@ -589,11 +599,17 @@ const createItemData: <T>(
|
||||
getItemTitle: (item: T) => string,
|
||||
items: T[],
|
||||
) => ItemData<T> = memoize((renderListItem, getItemTitle, items) => ({
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
renderListItem,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
getItemTitle,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
items,
|
||||
}));
|
||||
|
||||
// TODO: Too many non-null assertions
|
||||
|
||||
// @ts-expect-error "TODO: Understand and fix the type error here"
|
||||
const Row: <T>({
|
||||
index,
|
||||
@@ -613,12 +629,12 @@ const Row: <T>({
|
||||
],
|
||||
},
|
||||
}}
|
||||
title={getItemTitle(items[index])}
|
||||
title={getItemTitle(items[index]!)}
|
||||
placement="bottom-start"
|
||||
enterDelay={300}
|
||||
enterNextDelay={100}
|
||||
>
|
||||
<div style={style}>{renderListItem(items[index])}</div>
|
||||
<div style={style}>{renderListItem(items[index]!)}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@@ -634,7 +650,7 @@ function ItemList<T>(props: ItemListProps<T>) {
|
||||
|
||||
const getItemKey: ListItemKeySelector<ItemData<T>> = (index, data) => {
|
||||
const { items } = data;
|
||||
return props.generateItemKey(items[index]);
|
||||
return props.generateItemKey(items[index]!);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -642,11 +658,11 @@ function ItemList<T>(props: ItemListProps<T>) {
|
||||
<List
|
||||
itemData={itemData}
|
||||
height={Math.min(
|
||||
props.itemSize * props.items.length,
|
||||
props.maxHeight,
|
||||
props.itemSize! * props.items.length,
|
||||
props.maxHeight!,
|
||||
)}
|
||||
width={"100%"}
|
||||
itemSize={props.itemSize}
|
||||
itemSize={props.itemSize!}
|
||||
itemCount={props.items.length}
|
||||
itemKey={getItemKey}
|
||||
>
|
||||
@@ -657,15 +673,14 @@ function ItemList<T>(props: ItemListProps<T>) {
|
||||
}
|
||||
|
||||
const DoneFooter: React.FC = () => {
|
||||
const { uploadPhase, finishedUploads, retryFailed, onClose } = useContext(
|
||||
UploadProgressContext,
|
||||
);
|
||||
const { uploadPhase, finishedUploads, retryFailed, onClose } =
|
||||
useUploadProgressContext();
|
||||
|
||||
return (
|
||||
<DialogActions>
|
||||
{uploadPhase == "done" &&
|
||||
(finishedUploads?.get("failed")?.length > 0 ||
|
||||
finishedUploads?.get("blocked")?.length > 0 ? (
|
||||
((finishedUploads.get("failed")?.length ?? 0) > 0 ||
|
||||
(finishedUploads.get("blocked")?.length ?? 0) > 0 ? (
|
||||
<Button fullWidth onClick={retryFailed}>
|
||||
{t("retry_failed_uploads")}
|
||||
</Button>
|
||||
|
||||
@@ -72,7 +72,8 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
void isLocalStorageAndIndexedDBMismatch().then((mismatch) => {
|
||||
if (mismatch) {
|
||||
log.error("Logging out (IndexedDB and local storage mismatch)");
|
||||
return logout();
|
||||
logout();
|
||||
return;
|
||||
} else {
|
||||
return runMigrations();
|
||||
}
|
||||
@@ -130,9 +131,14 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
if (needsFamilyRedirect && savedPartialLocalUser()?.token)
|
||||
redirectToFamilyPortal();
|
||||
|
||||
router.events.on("routeChangeStart", (url) => {
|
||||
// Creating this inline, we need this on debug only and temporarily. Can
|
||||
// remove the debug print itself after a while.
|
||||
interface NROptions {
|
||||
shallow: boolean;
|
||||
}
|
||||
router.events.on("routeChangeStart", (url: string, o: NROptions) => {
|
||||
if (process.env.NEXT_PUBLIC_ENTE_TRACE_RT) {
|
||||
log.debug(() => ["route", url]);
|
||||
log.debug(() => [o?.shallow ? "route-shallow" : "route", url]);
|
||||
}
|
||||
|
||||
if (needsFamilyRedirect && savedPartialLocalUser()?.token) {
|
||||
|
||||
@@ -483,6 +483,7 @@ const Page: React.FC = () => {
|
||||
selected.ownCount++;
|
||||
}
|
||||
selected.count++;
|
||||
// @ts-expect-error Selection code needs type fixing
|
||||
selected[item.id] = true;
|
||||
});
|
||||
setSelected(selected);
|
||||
@@ -1075,8 +1076,10 @@ const Page: React.FC = () => {
|
||||
<GalleryBarAndListHeader
|
||||
{...{
|
||||
user,
|
||||
activeCollection,
|
||||
activeCollectionID,
|
||||
// TODO: These are incorrect assertions, the types of the
|
||||
// component need to be updated.
|
||||
activeCollection: activeCollection!,
|
||||
activeCollectionID: activeCollectionID!,
|
||||
activePerson,
|
||||
setFileListHeader,
|
||||
saveGroups,
|
||||
@@ -1162,7 +1165,8 @@ const Page: React.FC = () => {
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={activeCollectionID}
|
||||
// TODO: Incorrect assertion, need to update the type
|
||||
activeCollectionID={activeCollectionID!}
|
||||
activePersonID={activePerson?.id}
|
||||
isInIncomingSharedCollection={activeCollectionSummary?.attributes.has(
|
||||
"sharedIncoming",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// TODO: Audit this file
|
||||
// TODO: Audit this file (too many null assertions)
|
||||
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
@@ -95,7 +95,6 @@ import { type FileWithPath } from "react-dropzone";
|
||||
import { Trans } from "react-i18next";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import { getSelectedFiles, type SelectedState } from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
|
||||
export default function PublicCollectionGallery() {
|
||||
const { showMiniDialog, onGenericError } = useBaseContext();
|
||||
@@ -124,9 +123,9 @@ export default function PublicCollectionGallery() {
|
||||
context: undefined,
|
||||
});
|
||||
|
||||
// TODO: Can we convert these to state
|
||||
const credentials = useRef<PublicAlbumsCredentials | undefined>(undefined);
|
||||
const collectionKey = useRef<string>(null);
|
||||
const url = useRef<string>(null);
|
||||
const collectionKey = useRef<string | undefined>(undefined);
|
||||
|
||||
const { saveGroups, onAddSaveGroup, onRemoveSaveGroup } = useSaveGroups();
|
||||
|
||||
@@ -170,8 +169,7 @@ export default function PublicCollectionGallery() {
|
||||
const main = async () => {
|
||||
let redirectingToWebsite = false;
|
||||
try {
|
||||
url.current = window.location.href;
|
||||
const currentURL = new URL(url.current);
|
||||
const currentURL = new URL(window.location.href);
|
||||
const t = currentURL.searchParams.get("t");
|
||||
const ck = await extractCollectionKeyFromShareURL(currentURL);
|
||||
if (!t && !ck) {
|
||||
@@ -182,10 +180,7 @@ export default function PublicCollectionGallery() {
|
||||
return;
|
||||
}
|
||||
collectionKey.current = ck;
|
||||
url.current = window.location.href;
|
||||
const collection = await savedPublicCollectionByKey(
|
||||
collectionKey.current,
|
||||
);
|
||||
const collection = await savedPublicCollectionByKey(ck);
|
||||
const accessToken = t;
|
||||
let accessTokenJWT: string | undefined;
|
||||
if (collection) {
|
||||
@@ -227,12 +222,12 @@ export default function PublicCollectionGallery() {
|
||||
* both our local database and component state.
|
||||
*/
|
||||
const publicAlbumsRemotePull = useCallback(async () => {
|
||||
const accessToken = credentials.current.accessToken;
|
||||
const accessToken = credentials.current!.accessToken;
|
||||
showLoadingBar();
|
||||
setLoading(true);
|
||||
try {
|
||||
const { collection, referralCode: userReferralCode } =
|
||||
await pullCollection(accessToken, collectionKey.current);
|
||||
await pullCollection(accessToken, collectionKey.current!);
|
||||
setReferralCode(userReferralCode);
|
||||
|
||||
setPublicCollection(collection);
|
||||
@@ -243,18 +238,18 @@ export default function PublicCollectionGallery() {
|
||||
|
||||
// Remove the locally cached accessTokenJWT if the sharer has
|
||||
// disabled password protection on the link.
|
||||
if (!isPasswordProtected && credentials.current.accessTokenJWT) {
|
||||
if (!isPasswordProtected && credentials.current?.accessTokenJWT) {
|
||||
credentials.current.accessTokenJWT = undefined;
|
||||
downloadManager.setPublicAlbumsCredentials(credentials.current);
|
||||
removePublicCollectionAccessTokenJWT(accessToken);
|
||||
}
|
||||
|
||||
if (isPasswordProtected && !credentials.current.accessTokenJWT) {
|
||||
if (isPasswordProtected && !credentials.current?.accessTokenJWT) {
|
||||
await removePublicCollectionFileData(accessToken);
|
||||
} else {
|
||||
try {
|
||||
await pullPublicCollectionFiles(
|
||||
credentials.current,
|
||||
credentials.current!,
|
||||
collection,
|
||||
(files) =>
|
||||
setPublicFiles(
|
||||
@@ -273,7 +268,7 @@ export default function PublicCollectionGallery() {
|
||||
// Clear the locally cached accessTokenJWT and ask the user
|
||||
// to reenter the password.
|
||||
if (isHTTP401Error(e)) {
|
||||
credentials.current.accessTokenJWT = undefined;
|
||||
credentials.current!.accessTokenJWT = undefined;
|
||||
downloadManager.setPublicAlbumsCredentials(
|
||||
credentials.current,
|
||||
);
|
||||
@@ -304,7 +299,7 @@ export default function PublicCollectionGallery() {
|
||||
);
|
||||
// Sharing has been disabled. Clear out local cache.
|
||||
await removePublicCollectionFileData(accessToken);
|
||||
await removePublicCollectionByKey(collectionKey.current);
|
||||
await removePublicCollectionByKey(collectionKey.current!);
|
||||
setPublicCollection(undefined);
|
||||
setPublicFiles(undefined);
|
||||
} else {
|
||||
@@ -330,13 +325,13 @@ export default function PublicCollectionGallery() {
|
||||
setFieldError,
|
||||
) => {
|
||||
try {
|
||||
const accessToken = credentials.current.accessToken;
|
||||
const accessToken = credentials.current!.accessToken;
|
||||
const accessTokenJWT = await verifyPublicAlbumPassword(
|
||||
publicCollection.publicURLs[0]!,
|
||||
publicCollection!.publicURLs[0]!,
|
||||
password,
|
||||
accessToken,
|
||||
);
|
||||
credentials.current.accessTokenJWT = accessTokenJWT;
|
||||
credentials.current!.accessTokenJWT = accessTokenJWT;
|
||||
downloadManager.setPublicAlbumsCredentials(credentials.current);
|
||||
await savePublicCollectionAccessTokenJWT(
|
||||
accessToken,
|
||||
@@ -368,12 +363,12 @@ export default function PublicCollectionGallery() {
|
||||
|
||||
const handleUploadFile = (file: EnteFile) =>
|
||||
setPublicFiles(
|
||||
sortFilesForCollection([...publicFiles, file], publicCollection),
|
||||
sortFilesForCollection([...publicFiles!, file], publicCollection),
|
||||
);
|
||||
|
||||
const downloadFilesHelper = async () => {
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, publicFiles);
|
||||
const selectedFiles = getSelectedFiles(selected, publicFiles!);
|
||||
await downloadAndSaveFiles(
|
||||
selectedFiles,
|
||||
t("files_count", { count: selectedFiles.length }),
|
||||
@@ -432,7 +427,7 @@ export default function PublicCollectionGallery() {
|
||||
</Typography>
|
||||
</Stack100vhCenter>
|
||||
);
|
||||
} else if (isPasswordProtected && !credentials.current.accessTokenJWT) {
|
||||
} else if (isPasswordProtected && !credentials.current?.accessTokenJWT) {
|
||||
return (
|
||||
<AccountsPageContents>
|
||||
<AccountsPageTitle>{t("password")}</AccountsPageTitle>
|
||||
@@ -461,73 +456,69 @@ export default function PublicCollectionGallery() {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: memo this (after the dependencies are traceable).
|
||||
const context = { credentials: credentials.current };
|
||||
|
||||
return (
|
||||
<PublicCollectionGalleryContext.Provider value={context}>
|
||||
<FullScreenDropZone
|
||||
disabled={shouldDisableDropzone}
|
||||
onDrop={setDragAndDropFiles}
|
||||
<FullScreenDropZone
|
||||
disabled={shouldDisableDropzone}
|
||||
onDrop={setDragAndDropFiles}
|
||||
>
|
||||
<NavbarBase
|
||||
sx={{
|
||||
mb: "16px",
|
||||
px: "24px",
|
||||
"@media (width < 720px)": { px: "4px" },
|
||||
}}
|
||||
>
|
||||
<NavbarBase
|
||||
sx={{
|
||||
mb: "16px",
|
||||
px: "24px",
|
||||
"@media (width < 720px)": { px: "4px" },
|
||||
}}
|
||||
>
|
||||
{selected.count > 0 ? (
|
||||
<SelectedFileOptions
|
||||
count={selected.count}
|
||||
clearSelection={clearSelection}
|
||||
downloadFilesHelper={downloadFilesHelper}
|
||||
/>
|
||||
) : (
|
||||
<SpacedRow sx={{ flex: 1 }}>
|
||||
<EnteLogoLink href="https://ente.io">
|
||||
<EnteLogo height={15} />
|
||||
</EnteLogoLink>
|
||||
{onAddPhotos ? (
|
||||
<AddPhotosButton onClick={onAddPhotos} />
|
||||
) : (
|
||||
<GoToEnte />
|
||||
)}
|
||||
</SpacedRow>
|
||||
)}
|
||||
</NavbarBase>
|
||||
{selected.count > 0 ? (
|
||||
<SelectedFileOptions
|
||||
count={selected.count}
|
||||
clearSelection={clearSelection}
|
||||
downloadFilesHelper={downloadFilesHelper}
|
||||
/>
|
||||
) : (
|
||||
<SpacedRow sx={{ flex: 1 }}>
|
||||
<EnteLogoLink href="https://ente.io">
|
||||
<EnteLogo height={15} />
|
||||
</EnteLogoLink>
|
||||
{onAddPhotos ? (
|
||||
<AddPhotosButton onClick={onAddPhotos} />
|
||||
) : (
|
||||
<GoToEnte />
|
||||
)}
|
||||
</SpacedRow>
|
||||
)}
|
||||
</NavbarBase>
|
||||
|
||||
<FileListWithViewer
|
||||
files={publicFiles}
|
||||
header={fileListHeader}
|
||||
footer={fileListFooter}
|
||||
enableDownload={downloadEnabled}
|
||||
selectable={downloadEnabled}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={PseudoCollectionID.all}
|
||||
onRemotePull={publicAlbumsRemotePull}
|
||||
onVisualFeedback={handleVisualFeedback}
|
||||
onAddSaveGroup={onAddSaveGroup}
|
||||
/>
|
||||
{blockingLoad && <TranslucentLoadingOverlay />}
|
||||
<Upload
|
||||
uploadCollection={publicCollection}
|
||||
setLoading={setBlockingLoad}
|
||||
setShouldDisableDropzone={setShouldDisableDropzone}
|
||||
uploadTypeSelectorIntent="collect"
|
||||
uploadTypeSelectorView={uploadTypeSelectorView}
|
||||
onRemotePull={publicAlbumsRemotePull}
|
||||
onUploadFile={handleUploadFile}
|
||||
closeUploadTypeSelector={closeUploadTypeSelectorView}
|
||||
onShowSessionExpiredDialog={showPublicLinkExpiredMessage}
|
||||
{...{ dragAndDropFiles }}
|
||||
/>
|
||||
<DownloadStatusNotifications
|
||||
{...{ saveGroups, onRemoveSaveGroup }}
|
||||
/>
|
||||
</FullScreenDropZone>
|
||||
</PublicCollectionGalleryContext.Provider>
|
||||
<FileListWithViewer
|
||||
files={publicFiles}
|
||||
header={fileListHeader}
|
||||
footer={fileListFooter}
|
||||
enableDownload={downloadEnabled}
|
||||
selectable={downloadEnabled}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={PseudoCollectionID.all}
|
||||
onRemotePull={publicAlbumsRemotePull}
|
||||
onVisualFeedback={handleVisualFeedback}
|
||||
onAddSaveGroup={onAddSaveGroup}
|
||||
/>
|
||||
{blockingLoad && <TranslucentLoadingOverlay />}
|
||||
<Upload
|
||||
publicAlbumsCredentials={credentials.current}
|
||||
uploadCollection={publicCollection}
|
||||
setLoading={setBlockingLoad}
|
||||
setShouldDisableDropzone={setShouldDisableDropzone}
|
||||
uploadTypeSelectorIntent="collect"
|
||||
uploadTypeSelectorView={uploadTypeSelectorView}
|
||||
onRemotePull={publicAlbumsRemotePull}
|
||||
onUploadFile={handleUploadFile}
|
||||
closeUploadTypeSelector={closeUploadTypeSelectorView}
|
||||
onShowSessionExpiredDialog={showPublicLinkExpiredMessage}
|
||||
{...{ dragAndDropFiles }}
|
||||
/>
|
||||
<DownloadStatusNotifications
|
||||
{...{ saveGroups, onRemoveSaveGroup }}
|
||||
/>
|
||||
</FullScreenDropZone>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -688,7 +679,7 @@ interface FileListFooterProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* The dynamic (prop-depedent) height of {@link FileListFooter}.
|
||||
* The dynamic (prop-dependent) height of {@link FileListFooter}.
|
||||
*/
|
||||
const fileListFooterHeightForProps = ({
|
||||
referralCode,
|
||||
|
||||
@@ -235,7 +235,7 @@ class UIService {
|
||||
}
|
||||
}
|
||||
|
||||
function convertInProgressUploadsToList(inProgressUploads) {
|
||||
function convertInProgressUploadsToList(inProgressUploads: InProgressUploads) {
|
||||
return [...inProgressUploads.entries()].map(
|
||||
([localFileID, progress]) =>
|
||||
({ localFileID, progress }) as InProgressUpload,
|
||||
@@ -252,6 +252,7 @@ const groupByResult = (finishedUploads: FinishedUploads) => {
|
||||
};
|
||||
|
||||
class UploadManager {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
private comlinkCryptoWorkers: ComlinkWorker<typeof CryptoWorker>[] =
|
||||
new Array(maxConcurrentUploads);
|
||||
private parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
|
||||
@@ -297,6 +298,7 @@ class UploadManager {
|
||||
) {
|
||||
this.itemsToBeUploaded = [];
|
||||
this.failedItems = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.parsedMetadataJSONMap = parsedMetadataJSONMap ?? new Map();
|
||||
this.uploaderName = undefined;
|
||||
this.shouldUploadBeCancelled = false;
|
||||
@@ -668,11 +670,11 @@ type UploadItemWithCollectionIDAndName = UploadAsset & {
|
||||
const makeUploadItemWithCollectionIDAndName = (
|
||||
f: UploadItemWithCollection,
|
||||
): UploadItemWithCollectionIDAndName => ({
|
||||
localID: f.localID!,
|
||||
collectionID: f.collectionID!,
|
||||
fileName: (f.isLivePhoto
|
||||
localID: f.localID,
|
||||
collectionID: f.collectionID,
|
||||
fileName: f.isLivePhoto
|
||||
? uploadItemFileName(f.livePhotoAssets!.image)
|
||||
: uploadItemFileName(f.uploadItem!))!,
|
||||
: uploadItemFileName(f.uploadItem!),
|
||||
isLivePhoto: f.isLivePhoto,
|
||||
uploadItem: f.uploadItem,
|
||||
pathPrefix: f.pathPrefix,
|
||||
@@ -774,7 +776,9 @@ const logAboutMemoryPressureIfNeeded = () => {
|
||||
// is the method recommended by the Electron team (see the link about the V8
|
||||
// memory cage). The embedded Chromium supports it fine though, we just need
|
||||
// to goad TypeScript to accept the type.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const heapSize = (performance as any).memory.totalJSHeapSize;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const heapLimit = (performance as any).memory.jsHeapSizeLimit;
|
||||
if (heapSize / heapLimit > 0.7) {
|
||||
log.info(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: Audit this file
|
||||
import type { SelectionContext } from "ente-new/photos/components/gallery";
|
||||
import type { GalleryBarMode } from "ente-new/photos/components/gallery/reducer";
|
||||
import type { SelectedState, SetSelectedState } from "utils/file";
|
||||
@@ -10,14 +11,17 @@ export const handleSelectCreator =
|
||||
userID: number | undefined,
|
||||
activeCollectionID: number,
|
||||
activePersonID: string | undefined,
|
||||
// @ts-expect-error Need to add types
|
||||
setRangeStart?,
|
||||
) =>
|
||||
({ 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);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
setRangeStart(undefined);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +139,7 @@ const createSelectedAndContext = (
|
||||
context:
|
||||
mode == "people"
|
||||
? { mode, personID: activePersonID! }
|
||||
: { mode, collectionID: activeCollectionID! },
|
||||
: { mode, collectionID: activeCollectionID },
|
||||
};
|
||||
} else {
|
||||
// Both mode and context are defined.
|
||||
@@ -148,7 +152,7 @@ const createSelectedAndContext = (
|
||||
context:
|
||||
mode == "people"
|
||||
? { mode, personID: activePersonID! }
|
||||
: { mode, collectionID: activeCollectionID! },
|
||||
: { mode, collectionID: activeCollectionID },
|
||||
};
|
||||
} else {
|
||||
if (selected.context?.mode == "people") {
|
||||
@@ -173,7 +177,7 @@ const createSelectedAndContext = (
|
||||
collectionID: 0,
|
||||
context: {
|
||||
mode: selected.context?.mode,
|
||||
collectionID: activeCollectionID!,
|
||||
collectionID: activeCollectionID,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -185,7 +189,7 @@ const createSelectedAndContext = (
|
||||
? undefined
|
||||
: mode == "people"
|
||||
? { mode, personID: activePersonID! }
|
||||
: { mode, collectionID: activeCollectionID! };
|
||||
: { mode, collectionID: activeCollectionID };
|
||||
|
||||
return { selected, newContext };
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { PublicAlbumsCredentials } from "ente-base/http";
|
||||
import { createContext } from "react";
|
||||
|
||||
export interface PublicCollectionGalleryContextType {
|
||||
/**
|
||||
* The {@link PublicAlbumsCredentials} to use. These are guaranteed to be
|
||||
* set if we are in the context of the public albums app, and will be
|
||||
* undefined when we're in the default photos app context.
|
||||
*/
|
||||
credentials: PublicAlbumsCredentials | undefined;
|
||||
}
|
||||
|
||||
export const PublicCollectionGalleryContext =
|
||||
createContext<PublicCollectionGalleryContextType>({
|
||||
credentials: undefined,
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
// TODO: Audit this file
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
"extends": "ente-build-config/tsconfig-next.json",
|
||||
"compilerOptions": {
|
||||
/* Set the base directory from which to resolve bare module names. */
|
||||
"baseUrl": "./src",
|
||||
|
||||
/* Override tsconfig-next.json (TODO: Remove all of us) */
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false
|
||||
"baseUrl": "./src"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
@@ -202,7 +202,7 @@ const Page: React.FC = () => {
|
||||
// generated interactive key attributes to verify password.
|
||||
if (keyAttributes) {
|
||||
if (!user.token && !user.encryptedToken) {
|
||||
// TODO(RE): Why? For now, add a dev mode circuit breaker.
|
||||
// TODO: Why? For now, add a dev mode circuit breaker.
|
||||
if (isDevBuild) throw new Error("Unexpected case reached");
|
||||
clearLocalStorage();
|
||||
void router.replace("/");
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"extends": "ente-build-config/tsconfig-next.json",
|
||||
"compilerOptions": {
|
||||
/* MUI doesn't work with exactOptionalPropertyTypes yet. */
|
||||
"exactOptionalPropertyTypes": false
|
||||
},
|
||||
"include": [
|
||||
".",
|
||||
"../base/global-electron.d.ts",
|
||||
|
||||
@@ -198,7 +198,9 @@ class DownloadManager {
|
||||
* Set the credentials that should be used for download files when we're
|
||||
* running in the context of the public albums app.
|
||||
*/
|
||||
setPublicAlbumsCredentials(credentials: PublicAlbumsCredentials) {
|
||||
setPublicAlbumsCredentials(
|
||||
credentials: PublicAlbumsCredentials | undefined,
|
||||
) {
|
||||
this.publicAlbumsCredentials = credentials;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user