From 68d831ef3d356ea424539a7a30aa22992cf58463 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 10:37:40 +0530 Subject: [PATCH 01/11] Update --- web/apps/photos/src/components/FileList.tsx | 17 +-- web/apps/photos/src/components/Upload.tsx | 57 ++++---- web/apps/photos/src/pages/shared-albums.tsx | 127 +++++++++--------- .../utils/publicCollectionGallery/index.ts | 16 --- 4 files changed, 91 insertions(+), 126 deletions(-) delete mode 100644 web/apps/photos/src/utils/publicCollectionGallery/index.ts diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index e7eaa88228..a113a8adcf 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -30,7 +30,7 @@ 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, { useEffect, useMemo, useRef, useState } from "react"; import { VariableSizeList as List, type ListChildComponentProps, @@ -41,7 +41,6 @@ import { handleSelectCreator, handleSelectCreatorMulti, } from "utils/photoFrame"; -import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export const DATE_CONTAINER_HEIGHT = 48; export const SPACE_BTW_DATES = 44; @@ -210,10 +209,6 @@ export const FileList: React.FC = ({ emailByUserID, onItemClick, }) => { - const publicCollectionGalleryContext = useContext( - PublicCollectionGalleryContext, - ); - const [timeStampList, setTimeStampList] = useState([]); const refreshInProgress = useRef(false); const shouldRefresh = useRef(false); @@ -283,15 +278,7 @@ export const FileList: React.FC = ({ } }; main(); - }, [ - width, - height, - annotatedFiles, - header, - footer, - disableGrouping, - publicCollectionGalleryContext.credentials, - ]); + }, [width, height, header, footer, annotatedFiles, disableGrouping]); useEffect(() => { refreshList(); diff --git a/web/apps/photos/src/components/Upload.tsx b/web/apps/photos/src/components/Upload.tsx index 105d55a610..610df422da 100644 --- a/web/apps/photos/src/components/Upload.tsx +++ b/web/apps/photos/src/components/Upload.tsx @@ -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 = ({ user, + publicAlbumsCredentials, isFirstUpload, dragAndDropFiles, onRemotePull, @@ -177,9 +179,6 @@ export const Upload: React.FC = ({ }) => { const { showMiniDialog, onGenericError } = useBaseContext(); const { showNotification, watchFolderView } = usePhotosAppContext(); - const publicCollectionGalleryContext = useContext( - PublicCollectionGalleryContext, - ); const [uploadProgressView, setUploadProgressView] = useState(false); const [uploadPhase, setUploadPhase] = useState("preparing"); @@ -378,7 +377,7 @@ export const Upload: React.FC = ({ setUploadProgressView, }, onUploadFile, - publicCollectionGalleryContext.credentials, + publicAlbumsCredentials, ); if (uploadManager.isUploadRunning()) { @@ -408,7 +407,7 @@ export const Upload: React.FC = ({ 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 = ({ 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 = ({ onCancel: handleCollectionSelectorCancel, }); })(); - }, [webFiles, desktopFiles, desktopFilePaths, desktopZipItems]); + }, [ + publicAlbumsCredentials, + webFiles, + desktopFiles, + desktopFilePaths, + desktopZipItems, + ]); const preCollectionCreationAction = () => { onCloseCollectionSelector?.(); @@ -832,7 +837,7 @@ export const Upload: React.FC = ({ const handlePublicUpload = (uploaderName: string) => { savePublicCollectionUploaderName( - publicCollectionGalleryContext.credentials!.accessToken, + publicAlbumsCredentials!.accessToken, uploaderName, ); @@ -868,6 +873,7 @@ export const Upload: React.FC = ({ void; -}; +} & Pick; /** * 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 = ({ 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 diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 4ec23bae9f..8c3b4e648f 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -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(); @@ -461,73 +460,69 @@ export default function PublicCollectionGallery() { ); } - // TODO: memo this (after the dependencies are traceable). - const context = { credentials: credentials.current }; - return ( - - + - - {selected.count > 0 ? ( - - ) : ( - - - - - {onAddPhotos ? ( - - ) : ( - - )} - - )} - + {selected.count > 0 ? ( + + ) : ( + + + + + {onAddPhotos ? ( + + ) : ( + + )} + + )} + - - {blockingLoad && } - - - - + + {blockingLoad && } + + + ); } @@ -688,7 +683,7 @@ interface FileListFooterProps { } /** - * The dynamic (prop-depedent) height of {@link FileListFooter}. + * The dynamic (prop-dependent) height of {@link FileListFooter}. */ const fileListFooterHeightForProps = ({ referralCode, diff --git a/web/apps/photos/src/utils/publicCollectionGallery/index.ts b/web/apps/photos/src/utils/publicCollectionGallery/index.ts deleted file mode 100644 index 828134098c..0000000000 --- a/web/apps/photos/src/utils/publicCollectionGallery/index.ts +++ /dev/null @@ -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({ - credentials: undefined, - }); From 2a0795dd47460d7b32260459e360330c5a3bf329 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 10:50:35 +0530 Subject: [PATCH 02/11] null assertions meanwhile --- web/apps/photos/src/pages/shared-albums.tsx | 40 ++++++++++----------- web/packages/gallery/services/download.ts | 4 ++- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 8c3b4e648f..5ae52db264 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -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"; @@ -123,9 +123,9 @@ export default function PublicCollectionGallery() { context: undefined, }); + // TODO: Can we convert these to state const credentials = useRef(undefined); - const collectionKey = useRef(null); - const url = useRef(null); + const collectionKey = useRef(undefined); const { saveGroups, onAddSaveGroup, onRemoveSaveGroup } = useSaveGroups(); @@ -169,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) { @@ -181,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) { @@ -226,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); @@ -242,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( @@ -272,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, ); @@ -303,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 { @@ -329,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, @@ -367,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 }), @@ -431,7 +427,7 @@ export default function PublicCollectionGallery() { ); - } else if (isPasswordProtected && !credentials.current.accessTokenJWT) { + } else if (isPasswordProtected && !credentials.current?.accessTokenJWT) { return ( {t("password")} diff --git a/web/packages/gallery/services/download.ts b/web/packages/gallery/services/download.ts index fc90ed99fb..a4fb2e1b02 100644 --- a/web/packages/gallery/services/download.ts +++ b/web/packages/gallery/services/download.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; } From 786620a5aca4ee820cc8e87b712b2ceda4bc9e48 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 11:09:35 +0530 Subject: [PATCH 03/11] More null handling --- .../photos/src/components/UploadProgress.tsx | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index 1afa305ace..aaeafb6342 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -32,6 +32,7 @@ import { t } from "i18next"; import memoize from "memoize-one"; import React, { createContext, + useCallback, useContext, useEffect, useState, @@ -87,7 +88,7 @@ export const UploadProgress: React.FC = ({ if (open) setExpanded(false); }, [open]); - const handleClose = () => { + const handleClose = useCallback(() => { if (uploadPhase == "done") { onClose(); } else { @@ -102,7 +103,7 @@ export const UploadProgress: React.FC = ({ cancel: t("no"), }); } - }; + }, [uploadPhase, onClose, cancelUploads, showMiniDialog]); if (!open) { return <>; @@ -130,6 +131,9 @@ export const UploadProgress: React.FC = ({ ); }; +/** + * A context internal to the components of this file. + */ interface UploadProgressContextT { open: boolean; onClose: () => void; @@ -145,20 +149,19 @@ interface UploadProgressContextT { setExpanded: React.Dispatch>; } -const UploadProgressContext = createContext({ - 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( + 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 = () => ( @@ -176,9 +179,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 +203,8 @@ const UploadProgressTitle: React.FC = () => { }; const UploadProgressSubtitleText: React.FC = () => { - const { uploadPhase, uploadCounter, finishedUploads } = useContext( - UploadProgressContext, - ); + const { uploadPhase, uploadCounter, finishedUploads } = + useUploadProgressContext(); return ( { - const { uploadPhase, percentComplete } = useContext(UploadProgressContext); + const { uploadPhase, percentComplete } = useUploadProgressContext(); return ( @@ -300,9 +300,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,7 +376,8 @@ function UploadProgressDialog() { const InProgressSection: React.FC = () => { const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadPhase } = - useContext(UploadProgressContext); + useUploadProgressContext(); + const fileList = inProgressUploads ?? []; const renderListItem = ({ localFileID, progress }) => { @@ -492,9 +492,8 @@ const ResultSection: React.FC = ({ sectionTitle, sectionInfo, }) => { - const { finishedUploads, uploadFileNames } = useContext( - UploadProgressContext, - ); + const { finishedUploads, uploadFileNames } = useUploadProgressContext(); + const fileList = finishedUploads.get(resultType); if (!fileList?.length) { @@ -657,15 +656,14 @@ function ItemList(props: ItemListProps) { } const DoneFooter: React.FC = () => { - const { uploadPhase, finishedUploads, retryFailed, onClose } = useContext( - UploadProgressContext, - ); + const { uploadPhase, finishedUploads, retryFailed, onClose } = + useUploadProgressContext(); return ( {uploadPhase == "done" && - (finishedUploads?.get("failed")?.length > 0 || - finishedUploads?.get("blocked")?.length > 0 ? ( + ((finishedUploads.get("failed")?.length ?? 0) > 0 || + (finishedUploads.get("blocked")?.length ?? 0) > 0 ? ( From be7b57f3d5a2fd9506f7f361e85aee529cb98fb4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 11:31:32 +0530 Subject: [PATCH 04/11] Update --- web/apps/photos/src/components/FileList.tsx | 78 +++++++++++++-------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index a113a8adcf..e60ee9c149 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -42,11 +42,8 @@ import { handleSelectCreatorMulti, } from "utils/photoFrame"; -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}. @@ -252,6 +249,7 @@ export const FileList: React.FC = ({ if (header) { timeStampList.push(asFullSpanListItem(header)); } + if (disableGrouping) { noGrouping(timeStampList); } else { @@ -261,9 +259,21 @@ export const FileList: React.FC = ({ if (!skipMerge) { timeStampList = mergeTimeStampList(timeStampList, columns); } - if (timeStampList.length === 1) { - timeStampList.push(getEmptyListItem()); + + if (timeStampList.length == 1) { + timeStampList.push({ + item: ( + + + {t("nothing_here")} + + + ), + id: "empty-list-banner", + height: height - 48, + }); } + const footerHeight = footer?.height ?? 0; timeStampList.push(getVacuumItem(timeStampList, footerHeight)); if (footer) { @@ -278,7 +288,15 @@ export const FileList: React.FC = ({ } }; main(); - }, [width, height, header, footer, annotatedFiles, disableGrouping]); + }, [ + width, + height, + header, + footer, + annotatedFiles, + disableGrouping, + columns, + ]); useEffect(() => { refreshList(); @@ -337,20 +355,6 @@ export const FileList: React.FC = ({ }); }; - const getEmptyListItem = () => { - return { - item: ( - - - {t("nothing_here")} - - - ), - id: "empty-list-banner", - height: height - 48, - }; - }; - const getVacuumItem = (timeStampList, footerHeight: number) => { const fileListHeight = (() => { let sum = 0; @@ -387,6 +391,7 @@ export const FileList: React.FC = ({ // 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 + @@ -451,7 +456,7 @@ export const FileList: React.FC = ({ const getItemSize = (timeStampList) => (index) => { switch (timeStampList[index].tag) { case "date": - return DATE_CONTAINER_HEIGHT; + return dateContainerHeight; case "file": return listItemHeight; default: @@ -822,34 +827,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: {item}, }); -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; `; From 01d3c802402cbad526dc334234a1d28f8f6afbdd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 12:13:07 +0530 Subject: [PATCH 05/11] useDeferredValue --- web/apps/photos/src/components/FileList.tsx | 99 +++++++++++---------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index e60ee9c149..d3637d6766 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -30,7 +30,13 @@ 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, { useEffect, useMemo, useRef, useState } from "react"; +import React, { + useDeferredValue, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { VariableSizeList as List, type ListChildComponentProps, @@ -206,9 +212,11 @@ export const FileList: React.FC = ({ emailByUserID, onItemClick, }) => { - const [timeStampList, setTimeStampList] = useState([]); - const refreshInProgress = useRef(false); - const shouldRefresh = useRef(false); + const [_timeStampList, setTimeStampList] = useState( + new Array(), + ); + const timeStampList = useDeferredValue(_timeStampList); + const listRef = useRef(null); // Timeline date strings for which all photos have been selected. @@ -238,56 +246,51 @@ export const FileList: React.FC = ({ }; 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)); - } + let timeStampList: TimeStampListItem[] = []; - if (disableGrouping) { - noGrouping(timeStampList); - } else { - groupByTime(timeStampList); - } + if (header) { + timeStampList.push(asFullSpanListItem(header)); + } - if (!skipMerge) { - timeStampList = mergeTimeStampList(timeStampList, columns); - } + if (disableGrouping) { + noGrouping(timeStampList); + } else { + groupByTime(timeStampList); + } - if (timeStampList.length == 1) { - timeStampList.push({ - item: ( - - - {t("nothing_here")} - - - ), - id: "empty-list-banner", - height: height - 48, - }); - } + if (!skipMerge) { + timeStampList = mergeTimeStampList(timeStampList, columns); + } - const footerHeight = footer?.height ?? 0; - timeStampList.push(getVacuumItem(timeStampList, footerHeight)); - if (footer) { - timeStampList.push(asFullSpanListItem(footer)); - } + if (timeStampList.length == 1) { + timeStampList.push({ + item: ( + + + {t("nothing_here")} + + + ), + id: "empty-list-banner", + height: height - 48, + }); + } - setTimeStampList(timeStampList); - refreshInProgress.current = false; - if (shouldRefresh.current) { - shouldRefresh.current = false; - setTimeout(main, 0); - } - }; - main(); + const footerHeight = footer?.height ?? 0; + timeStampList.push(getVacuumItem(timeStampList, footerHeight)); + if (footer) { + timeStampList.push(asFullSpanListItem(footer)); + } + + setTimeStampList(timeStampList); }, [ width, height, From 67b9ba09fa65484a4bf8f3bce4e4be98a149537e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 12:21:23 +0530 Subject: [PATCH 06/11] Update --- web/apps/photos/src/components/FileList.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index d3637d6766..bcf9d34246 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -38,8 +38,8 @@ import React, { useState, } from "react"; import { - VariableSizeList as List, type ListChildComponentProps, + VariableSizeList, areEqual, } from "react-window"; import { type SelectedState, shouldShowAvatar } from "utils/file"; @@ -217,7 +217,7 @@ export const FileList: React.FC = ({ ); const timeStampList = useDeferredValue(_timeStampList); - const listRef = useRef(null); + const listRef = useRef(null); // Timeline date strings for which all photos have been selected. // @@ -747,7 +747,7 @@ export const FileList: React.FC = ({ } return ( - = ({ useIsScrolling > {PhotoListRow} - + ); }; From a34a07644e4415f67babf2ed5c7ca39ad31bfae4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 12:30:26 +0530 Subject: [PATCH 07/11] tsc --- web/apps/photos/src/components/FileList.tsx | 104 ++++++++++---------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index bcf9d34246..aa05c2d6d8 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -73,7 +73,7 @@ interface TimeStampListItem { tag?: "date" | "file"; items?: FileListAnnotatedFile[]; itemStartIndex?: number; - date?: string; + date?: string | null; dates?: { date: string; span: number }[]; groups?: number[]; item?: any; @@ -225,8 +225,8 @@ export const FileList: React.FC = ({ const [checkedTimelineDateStrings, setCheckedTimelineDateStrings] = useState(new Set()); - const [rangeStart, setRangeStart] = useState(null); - const [currentHover, setCurrentHover] = useState(null); + const [rangeStart, setRangeStart] = useState(null); + const [currentHover, setCurrentHover] = useState(null); const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const fittableColumns = getFractionFittableColumns(width); @@ -237,14 +237,11 @@ export const FileList: React.FC = ({ 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(() => { // Since width and height are dependencies, there might be too many // updates to the list during a resize. The list computation too, while @@ -302,9 +299,12 @@ export const FileList: React.FC = ({ ]); 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; @@ -328,7 +328,7 @@ export const FileList: React.FC = ({ }); listItemIndex = 1; } else if (listItemIndex < columns) { - timeStampList[timeStampList.length - 1].items.push(item); + timeStampList[timeStampList.length - 1]!.items!.push(item); listItemIndex++; } else { listItemIndex = 1; @@ -345,7 +345,7 @@ export const FileList: React.FC = ({ 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; @@ -387,7 +387,7 @@ export const FileList: React.FC = ({ 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") { @@ -397,21 +397,21 @@ export const FileList: React.FC = ({ 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 { @@ -427,12 +427,12 @@ export const FileList: React.FC = ({ 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 { @@ -444,11 +444,11 @@ export const FileList: React.FC = ({ } } 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; } } @@ -467,14 +467,14 @@ export const FileList: React.FC = ({ } }; - 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}`; } }; @@ -558,23 +558,23 @@ export const FileList: React.FC = ({ 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); } }; @@ -616,7 +616,7 @@ export const FileList: React.FC = ({ {...{ user, emailByUserID }} file={file} onClick={() => onItemClick(index)} - selectable={selectable} + selectable={selectable!} onSelect={handleSelect(file, index)} selected={ (!mode @@ -632,8 +632,8 @@ export const FileList: React.FC = ({ 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} @@ -643,7 +643,7 @@ export const FileList: React.FC = ({ const renderListItem = ( listItem: TimeStampListItem, - isScrolling: boolean, + isScrolling: boolean | undefined, ) => { const haveSelection = (selected.count ?? 0) > 0; switch (listItem.tag) { @@ -676,12 +676,12 @@ export const FileList: React.FC = ({ {haveSelection && ( - onChangeSelectAllCheckBox(listItem.date) + onChangeSelectAllCheckBox(listItem.date!) } size="small" sx={{ pl: 0 }} @@ -691,22 +691,22 @@ export const FileList: React.FC = ({ ); 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,
, ); sum += 1; @@ -911,10 +911,10 @@ const PhotoListRow = React.memo( gridTemplateColumns={getTemplateColumns( columns, shrinkRatio, - timeStampList[index].groups, + timeStampList[index]!.groups, )} > - {renderListItem(timeStampList[index], isScrolling)} + {renderListItem(timeStampList[index]!, isScrolling)} ); @@ -935,7 +935,7 @@ type FileThumbnailProps = { isInsSelectRange: boolean; activeCollectionID: number; showPlaceholder: boolean; - isFav: boolean; + isFav: boolean | undefined; } & Pick; const FileThumbnail: React.FC = ({ From 6d20b9cd5516b0a8f8c350ba4da2d376629dc57e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 12:35:20 +0530 Subject: [PATCH 08/11] Elsewhere --- .../components/Collections/AlbumCastDialog.tsx | 1 + .../Collections/GalleryBarAndListHeader.tsx | 10 +++++----- .../photos/src/components/UploadProgress.tsx | 17 ++++++++++------- web/apps/photos/src/pages/gallery.tsx | 10 +++++++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx index aa1e972358..915fb693fb 100644 --- a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx @@ -87,6 +87,7 @@ export const AlbumCastDialogContents: React.FC = ({ // (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"); }, []); diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx index 1bf7574f06..65c0aeb2bd 100644 --- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx +++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx @@ -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(() => { @@ -146,8 +146,8 @@ export const GalleryBarAndListHeader: React.FC< onAddSaveGroup, }} collectionSummary={toShowCollectionSummaries.get( - activeCollectionID, - )} + activeCollectionID!, + )!} onCollectionShare={showCollectionShare} onCollectionCast={showCollectionCast} /> @@ -212,8 +212,8 @@ export const GalleryBarAndListHeader: React.FC< = ({ }; const getItemTitle = (fileID) => { - return uploadFileNames.get(fileID); + return uploadFileNames.get(fileID)!; }; const generateItemKey = (fileID) => { @@ -593,6 +594,8 @@ const createItemData: ( items, })); +// TODO: Too many non-null assertions + // @ts-expect-error "TODO: Understand and fix the type error here" const Row: ({ index, @@ -612,12 +615,12 @@ const Row: ({ ], }, }} - title={getItemTitle(items[index])} + title={getItemTitle(items[index]!)} placement="bottom-start" enterDelay={300} enterNextDelay={100} > -
{renderListItem(items[index])}
+
{renderListItem(items[index]!)}
); }, @@ -633,7 +636,7 @@ function ItemList(props: ItemListProps) { const getItemKey: ListItemKeySelector> = (index, data) => { const { items } = data; - return props.generateItemKey(items[index]); + return props.generateItemKey(items[index]!); }; return ( @@ -641,11 +644,11 @@ function ItemList(props: ItemListProps) { diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index dbb6f1425c..9a47329755 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -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 = () => { { 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", From 3fb02cf343595e0e46dee58ff5d61f1e0eb29b4b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 12:49:18 +0530 Subject: [PATCH 09/11] Remove overrides --- .../src/components/Collections/AllAlbums.tsx | 10 ++++++- .../Collections/GalleryBarAndListHeader.tsx | 12 ++++---- web/apps/photos/src/components/FileList.tsx | 29 ++++++++++--------- .../photos/src/components/UploadProgress.tsx | 6 ++++ web/apps/photos/src/pages/_app.tsx | 3 +- .../photos/src/services/upload-manager.ts | 1 + web/apps/photos/src/utils/photoFrame/index.ts | 2 ++ web/apps/photos/tsconfig.json | 6 +--- web/packages/accounts/tsconfig.json | 4 --- 9 files changed, 43 insertions(+), 30 deletions(-) diff --git a/web/apps/photos/src/components/Collections/AllAlbums.tsx b/web/apps/photos/src/components/Collections/AllAlbums.tsx index e0b6bf710f..be7f7e7921 100644 --- a/web/apps/photos/src/components/Collections/AllAlbums.tsx +++ b/web/apps/photos/src/components/Collections/AllAlbums.tsx @@ -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 = ({ onClose, collectionCount, collectionsSortBy, diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx index 65c0aeb2bd..053e67aee9 100644 --- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx +++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx @@ -145,9 +145,9 @@ export const GalleryBarAndListHeader: React.FC< onRemotePull, onAddSaveGroup, }} - collectionSummary={toShowCollectionSummaries.get( - activeCollectionID!, - )!} + collectionSummary={ + toShowCollectionSummaries.get(activeCollectionID!)! + } onCollectionShare={showCollectionShare} onCollectionCast={showCollectionCast} /> @@ -211,9 +211,9 @@ export const GalleryBarAndListHeader: React.FC< /> = ({ }); }; - const getVacuumItem = (timeStampList, footerHeight: number) => { + const getVacuumItem = ( + timeStampList: TimeStampListItem[], + footerHeight: number, + ) => { const fileListHeight = (() => { let sum = 0; const getCurrentItemSize = getItemSize(timeStampList); for (let i = 0; i < timeStampList.length; i++) { - sum += getCurrentItemSize(i); + sum += getCurrentItemSize(i)!; if (height - sum <= footerHeight) { break; } @@ -456,16 +458,17 @@ export const FileList: React.FC = ({ return newList; }; - const getItemSize = (timeStampList) => (index) => { - switch (timeStampList[index].tag) { - case "date": - return dateContainerHeight; - 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: number) => { switch (timeStampList[index]!.tag) { diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index 100cd2b9b8..29a4c436ab 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -381,6 +381,7 @@ const InProgressSection: React.FC = () => { const fileList = inProgressUploads ?? []; + // @ts-expect-error Need to add types const renderListItem = ({ localFileID, progress }) => { return ( @@ -396,10 +397,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}`; }; @@ -501,6 +504,7 @@ const ResultSection: React.FC = ({ return <>; } + // @ts-expect-error Need to add types const renderListItem = (fileID) => { return ( @@ -509,10 +513,12 @@ const ResultSection: React.FC = ({ ); }; + // @ts-expect-error Need to add types const getItemTitle = (fileID) => { return uploadFileNames.get(fileID)!; }; + // @ts-expect-error Need to add types const generateItemKey = (fileID) => { return fileID; }; diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 6531ecd47d..9d7a2b4dcc 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -72,7 +72,8 @@ const App: React.FC = ({ Component, pageProps }) => { void isLocalStorageAndIndexedDBMismatch().then((mismatch) => { if (mismatch) { log.error("Logging out (IndexedDB and local storage mismatch)"); - return logout(); + logout(); + return; } else { return runMigrations(); } diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index e4d0d4e6a1..07ed999501 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -235,6 +235,7 @@ class UIService { } } +// @ts-expect-error Need to add types function convertInProgressUploadsToList(inProgressUploads) { return [...inProgressUploads.entries()].map( ([localFileID, progress]) => diff --git a/web/apps/photos/src/utils/photoFrame/index.ts b/web/apps/photos/src/utils/photoFrame/index.ts index d5157febef..e5a73dde20 100644 --- a/web/apps/photos/src/utils/photoFrame/index.ts +++ b/web/apps/photos/src/utils/photoFrame/index.ts @@ -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,6 +11,7 @@ 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) => diff --git a/web/apps/photos/tsconfig.json b/web/apps/photos/tsconfig.json index 4b0f70e320..6360403f00 100644 --- a/web/apps/photos/tsconfig.json +++ b/web/apps/photos/tsconfig.json @@ -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", diff --git a/web/packages/accounts/tsconfig.json b/web/packages/accounts/tsconfig.json index 032194f233..15dcfedc2b 100644 --- a/web/packages/accounts/tsconfig.json +++ b/web/packages/accounts/tsconfig.json @@ -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", From 6e025945ae9089c97b078e185e035f5fde800b9c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 13:05:37 +0530 Subject: [PATCH 10/11] es --- web/apps/photos/eslint.config.mjs | 3 --- .../photos/src/components/Collections/AlbumCastDialog.tsx | 3 +++ web/apps/photos/src/components/Collections/AllAlbums.tsx | 5 ++++- web/apps/photos/src/components/UploadProgress.tsx | 7 +++++++ web/apps/photos/src/services/upload-manager.ts | 7 +++++-- web/apps/photos/src/utils/photoFrame/index.ts | 2 ++ web/apps/photos/tests/upload.test.ts | 3 +++ 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/eslint.config.mjs b/web/apps/photos/eslint.config.mjs index 76d9b9a84c..55cdf1eea7 100644 --- a/web/apps/photos/eslint.config.mjs +++ b/web/apps/photos/eslint.config.mjs @@ -11,12 +11,9 @@ export default [ */ "@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", diff --git a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx index 915fb693fb..9e255bdab5 100644 --- a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx @@ -128,7 +128,10 @@ export const AlbumCastDialogContents: React.FC = ({ "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 const code = obj.code; if (code) { diff --git a/web/apps/photos/src/components/Collections/AllAlbums.tsx b/web/apps/photos/src/components/Collections/AllAlbums.tsx index be7f7e7921..f7d54286ca 100644 --- a/web/apps/photos/src/components/Collections/AllAlbums.tsx +++ b/web/apps/photos/src/components/Collections/AllAlbums.tsx @@ -162,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, })); @@ -182,7 +185,7 @@ const AlbumsRow = React.memo( return (
- {collectionRow.map((item: any) => ( + {collectionRow.map((item) => ( { // @ts-expect-error Need to add types const renderListItem = ({ localFileID, progress }) => { return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment {uploadFileNames.get(localFileID)} {uploadPhase == "uploading" && ( @@ -507,6 +509,7 @@ const ResultSection: React.FC = ({ // @ts-expect-error Need to add types const renderListItem = (fileID) => { return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment {uploadFileNames.get(fileID)} @@ -595,8 +598,12 @@ const createItemData: ( getItemTitle: (item: T) => string, items: T[], ) => ItemData = 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, })); diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 07ed999501..7df566064a 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -235,8 +235,7 @@ class UIService { } } -// @ts-expect-error Need to add types -function convertInProgressUploadsToList(inProgressUploads) { +function convertInProgressUploadsToList(inProgressUploads: InProgressUploads) { return [...inProgressUploads.entries()].map( ([localFileID, progress]) => ({ localFileID, progress }) as InProgressUpload, @@ -253,6 +252,7 @@ const groupByResult = (finishedUploads: FinishedUploads) => { }; class UploadManager { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment private comlinkCryptoWorkers: ComlinkWorker[] = new Array(maxConcurrentUploads); private parsedMetadataJSONMap = new Map(); @@ -298,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; @@ -775,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 const heapSize = (performance as any).memory.totalJSHeapSize; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const heapLimit = (performance as any).memory.jsHeapSizeLimit; if (heapSize / heapLimit > 0.7) { log.info( diff --git a/web/apps/photos/src/utils/photoFrame/index.ts b/web/apps/photos/src/utils/photoFrame/index.ts index e5a73dde20..c32a382df1 100644 --- a/web/apps/photos/src/utils/photoFrame/index.ts +++ b/web/apps/photos/src/utils/photoFrame/index.ts @@ -18,8 +18,10 @@ export const handleSelectCreator = (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); } } diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 7f1bbcfd7e..87bc447531 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -1,4 +1,7 @@ // 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 */ From 96276a123411ff530b34bbf0d044ba64d337f688 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 8 Jul 2025 13:20:11 +0530 Subject: [PATCH 11/11] es --- web/apps/photos/eslint.config.mjs | 4 ---- .../src/components/Collections/AlbumCastDialog.tsx | 4 ++-- .../Collections/GalleryBarAndListHeader.tsx | 2 +- web/apps/photos/src/components/FileList.tsx | 4 +++- web/apps/photos/src/components/UploadProgress.tsx | 1 + web/apps/photos/src/pages/_app.tsx | 9 +++++++-- web/apps/photos/src/services/upload-manager.ts | 12 ++++++------ web/apps/photos/src/utils/photoFrame/index.ts | 8 ++++---- web/apps/photos/tests/upload.test.ts | 1 + web/packages/accounts/pages/credentials.tsx | 2 +- 10 files changed, 26 insertions(+), 21 deletions(-) diff --git a/web/apps/photos/eslint.config.mjs b/web/apps/photos/eslint.config.mjs index 55cdf1eea7..8765bfa62e 100644 --- a/web/apps/photos/eslint.config.mjs +++ b/web/apps/photos/eslint.config.mjs @@ -9,14 +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-explicit-any": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unsafe-member-access": "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", }, }, diff --git a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx index 9e255bdab5..f1e300335f 100644 --- a/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/AlbumCastDialog.tsx @@ -87,7 +87,7 @@ export const AlbumCastDialogContents: React.FC = ({ // (effectively, only Chrome). // // Override, otherwise tsc complains about unknown property `chrome`. - // @ts-expect-error TODO why is this needed + // @ts-expect-error TODO: why is this needed // eslint-disable-next-line @typescript-eslint/dot-notation setBrowserCanCast(typeof window["chrome"] != "undefined"); }, []); @@ -131,7 +131,7 @@ export const AlbumCastDialogContents: React.FC = ({ // TODO: // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const obj = JSON.parse(data); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const code = obj.code; if (code) { diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx index 053e67aee9..b6be9a1ad9 100644 --- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx +++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx @@ -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, ]); diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index e0f25a98ae..f75bb4da11 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -365,7 +365,7 @@ export const FileList: React.FC = ({ let sum = 0; const getCurrentItemSize = getItemSize(timeStampList); for (let i = 0; i < timeStampList.length; i++) { - sum += getCurrentItemSize(i)!; + sum += getCurrentItemSize(i); if (height - sum <= footerHeight) { break; } @@ -718,6 +718,8 @@ export const FileList: React.FC = ({ return ret; } default: + // TODO: + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return listItem.item; } }; diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index e8a4355ac3..d2a2aec71b 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -523,6 +523,7 @@ const ResultSection: React.FC = ({ // @ts-expect-error Need to add types const generateItemKey = (fileID) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return fileID; }; diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 9d7a2b4dcc..3ae9eb6abe 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -131,9 +131,14 @@ const App: React.FC = ({ 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) { diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 7df566064a..f4ff2d8e1c 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -670,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, @@ -776,9 +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 + // 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 + // 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( diff --git a/web/apps/photos/src/utils/photoFrame/index.ts b/web/apps/photos/src/utils/photoFrame/index.ts index c32a382df1..96ed15fa6c 100644 --- a/web/apps/photos/src/utils/photoFrame/index.ts +++ b/web/apps/photos/src/utils/photoFrame/index.ts @@ -139,7 +139,7 @@ const createSelectedAndContext = ( context: mode == "people" ? { mode, personID: activePersonID! } - : { mode, collectionID: activeCollectionID! }, + : { mode, collectionID: activeCollectionID }, }; } else { // Both mode and context are defined. @@ -152,7 +152,7 @@ const createSelectedAndContext = ( context: mode == "people" ? { mode, personID: activePersonID! } - : { mode, collectionID: activeCollectionID! }, + : { mode, collectionID: activeCollectionID }, }; } else { if (selected.context?.mode == "people") { @@ -177,7 +177,7 @@ const createSelectedAndContext = ( collectionID: 0, context: { mode: selected.context?.mode, - collectionID: activeCollectionID!, + collectionID: activeCollectionID, }, }; } @@ -189,7 +189,7 @@ const createSelectedAndContext = ( ? undefined : mode == "people" ? { mode, personID: activePersonID! } - : { mode, collectionID: activeCollectionID! }; + : { mode, collectionID: activeCollectionID }; return { selected, newContext }; }; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 87bc447531..9a2d2ddfe0 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -1,3 +1,4 @@ +/* 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 */ diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index 2fa37c73c4..aab0ae6d1b 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -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("/");