Browse Source

[desktop] Fix watch related IPC - Part 1/x (#1463)

Manav Rathi 1 year ago
parent
commit
bb9c384a52

+ 3 - 3
desktop/src/main/ipc.ts

@@ -10,7 +10,7 @@
 
 import type { FSWatcher } from "chokidar";
 import { ipcMain } from "electron/main";
-import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
+import type { ElectronFile, FILE_PATH_TYPE, FolderWatch } from "../types/ipc";
 import {
     selectDirectory,
     showUploadDirsDialog,
@@ -242,13 +242,13 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
 
     ipcMain.handle(
         "updateWatchMappingSyncedFiles",
-        (_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
+        (_, folderPath: string, files: FolderWatch["syncedFiles"]) =>
             updateWatchMappingSyncedFiles(folderPath, files),
     );
 
     ipcMain.handle(
         "updateWatchMappingIgnoredFiles",
-        (_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
+        (_, folderPath: string, files: FolderWatch["ignoredFiles"]) =>
             updateWatchMappingIgnoredFiles(folderPath, files),
     );
 };

+ 4 - 4
desktop/src/main/services/watch.ts

@@ -1,6 +1,6 @@
 import type { FSWatcher } from "chokidar";
 import ElectronLog from "electron-log";
-import { WatchMapping, WatchStoreType } from "../../types/ipc";
+import { FolderWatch, WatchStoreType } from "../../types/ipc";
 import { watchStore } from "../stores/watch.store";
 
 export const addWatchMapping = async (
@@ -28,7 +28,7 @@ export const addWatchMapping = async (
     setWatchMappings(watchMappings);
 };
 
-function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
+function isMappingPresent(watchMappings: FolderWatch[], folderPath: string) {
     const watchMapping = watchMappings?.find(
         (mapping) => mapping.folderPath === folderPath,
     );
@@ -59,7 +59,7 @@ export const removeWatchMapping = async (
 
 export function updateWatchMappingSyncedFiles(
     folderPath: string,
-    files: WatchMapping["syncedFiles"],
+    files: FolderWatch["syncedFiles"],
 ): void {
     const watchMappings = getWatchMappings();
     const watchMapping = watchMappings.find(
@@ -76,7 +76,7 @@ export function updateWatchMappingSyncedFiles(
 
 export function updateWatchMappingIgnoredFiles(
     folderPath: string,
-    files: WatchMapping["ignoredFiles"],
+    files: FolderWatch["ignoredFiles"],
 ): void {
     const watchMappings = getWatchMappings();
     const watchMapping = watchMappings.find(

+ 4 - 4
desktop/src/preload.ts

@@ -45,7 +45,7 @@ import type {
     AppUpdateInfo,
     ElectronFile,
     FILE_PATH_TYPE,
-    WatchMapping,
+    FolderWatch,
 } from "./types/ipc";
 
 // - General
@@ -220,18 +220,18 @@ const addWatchMapping = (
 const removeWatchMapping = (folderPath: string): Promise<void> =>
     ipcRenderer.invoke("removeWatchMapping", folderPath);
 
-const getWatchMappings = (): Promise<WatchMapping[]> =>
+const getWatchMappings = (): Promise<FolderWatch[]> =>
     ipcRenderer.invoke("getWatchMappings");
 
 const updateWatchMappingSyncedFiles = (
     folderPath: string,
-    files: WatchMapping["syncedFiles"],
+    files: FolderWatch["syncedFiles"],
 ): Promise<void> =>
     ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
 
 const updateWatchMappingIgnoredFiles = (
     folderPath: string,
-    files: WatchMapping["ignoredFiles"],
+    files: FolderWatch["ignoredFiles"],
 ): Promise<void> =>
     ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
 

+ 15 - 15
desktop/src/types/ipc.ts

@@ -5,6 +5,20 @@
  * See [Note: types.ts <-> preload.ts <-> ipc.ts]
  */
 
+export interface FolderWatch {
+    rootFolderName: string;
+    uploadStrategy: number;
+    folderPath: string;
+    syncedFiles: FolderWatchSyncedFile[];
+    ignoredFiles: string[];
+}
+
+export interface FolderWatchSyncedFile {
+    path: string;
+    uploadedFileID: number;
+    collectionID: number;
+}
+
 /**
  * Errors that have special semantics on the web side.
  *
@@ -52,22 +66,8 @@ export interface ElectronFile {
     arrayBuffer: () => Promise<Uint8Array>;
 }
 
-interface WatchMappingSyncedFile {
-    path: string;
-    uploadedFileID: number;
-    collectionID: number;
-}
-
-export interface WatchMapping {
-    rootFolderName: string;
-    uploadStrategy: number;
-    folderPath: string;
-    syncedFiles: WatchMappingSyncedFile[];
-    ignoredFiles: string[];
-}
-
 export interface WatchStoreType {
-    mappings: WatchMapping[];
+    mappings: FolderWatch[];
 }
 
 export enum FILE_PATH_TYPE {

+ 1 - 1
web/apps/photos/src/components/Sidebar/UtilitySection.tsx

@@ -24,7 +24,7 @@ import {
 import { getAccountsURL } from "@ente/shared/network/api";
 import { THEME_COLOR } from "@ente/shared/themes/constants";
 import { EnteMenuItem } from "components/Menu/EnteMenuItem";
-import WatchFolder from "components/WatchFolder";
+import { WatchFolder } from "components/WatchFolder";
 import isElectron from "is-electron";
 import { getAccountsToken } from "services/userService";
 import { getDownloadAppMessage } from "utils/ui";

+ 1 - 1
web/apps/photos/src/components/Upload/Uploader.tsx

@@ -24,7 +24,7 @@ import {
     savePublicCollectionUploaderName,
 } from "services/publicCollectionService";
 import uploadManager from "services/upload/uploadManager";
-import watchFolderService from "services/watchFolder/watchFolderService";
+import watchFolderService from "services/watch";
 import { NotificationAttributes } from "types/Notification";
 import { Collection } from "types/collection";
 import {

+ 364 - 0
web/apps/photos/src/components/WatchFolder.tsx

@@ -0,0 +1,364 @@
+import {
+    FlexWrapper,
+    HorizontalFlex,
+    SpaceBetweenFlex,
+    VerticallyCentered,
+} from "@ente/shared/components/Container";
+import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
+import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
+import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
+import CheckIcon from "@mui/icons-material/Check";
+import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
+import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
+import FolderOpenIcon from "@mui/icons-material/FolderOpen";
+import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
+import {
+    Box,
+    Button,
+    CircularProgress,
+    Dialog,
+    DialogContent,
+    Stack,
+    Tooltip,
+    Typography,
+} from "@mui/material";
+import { styled } from "@mui/material/styles";
+import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
+import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
+import { t } from "i18next";
+import { AppContext } from "pages/_app";
+import React, { useContext, useEffect, useState } from "react";
+import watchFolderService from "services/watch";
+import { WatchMapping } from "types/watchFolder";
+import { getImportSuggestion } from "utils/upload";
+
+interface WatchFolderProps {
+    open: boolean;
+    onClose: () => void;
+}
+
+export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
+    const [mappings, setMappings] = useState<WatchMapping[]>([]);
+    const [inputFolderPath, setInputFolderPath] = useState("");
+    const [choiceModalOpen, setChoiceModalOpen] = useState(false);
+    const appContext = useContext(AppContext);
+
+    const electron = globalThis.electron;
+
+    useEffect(() => {
+        if (!electron) return;
+        watchFolderService.getWatchMappings().then((m) => setMappings(m));
+    }, []);
+
+    useEffect(() => {
+        if (
+            appContext.watchFolderFiles &&
+            appContext.watchFolderFiles.length > 0
+        ) {
+            handleFolderDrop(appContext.watchFolderFiles);
+            appContext.setWatchFolderFiles(null);
+        }
+    }, [appContext.watchFolderFiles]);
+
+    const handleFolderDrop = async (folders: FileList) => {
+        for (let i = 0; i < folders.length; i++) {
+            const folder: any = folders[i];
+            const path = (folder.path as string).replace(/\\/g, "/");
+            if (await watchFolderService.isFolder(path)) {
+                await addFolderForWatching(path);
+            }
+        }
+    };
+
+    const addFolderForWatching = async (path: string) => {
+        if (!electron) return;
+
+        setInputFolderPath(path);
+        const files = await electron.getDirFiles(path);
+        const analysisResult = getImportSuggestion(
+            PICKED_UPLOAD_TYPE.FOLDERS,
+            files,
+        );
+        if (analysisResult.hasNestedFolders) {
+            setChoiceModalOpen(true);
+        } else {
+            handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
+        }
+    };
+
+    const handleAddFolderClick = async () => {
+        await handleFolderSelection();
+    };
+
+    const handleFolderSelection = async () => {
+        const folderPath = await watchFolderService.selectFolder();
+        if (folderPath) {
+            await addFolderForWatching(folderPath);
+        }
+    };
+
+    const handleAddWatchMapping = async (
+        uploadStrategy: UPLOAD_STRATEGY,
+        folderPath?: string,
+    ) => {
+        folderPath = folderPath || inputFolderPath;
+        await watchFolderService.addWatchMapping(
+            folderPath.substring(folderPath.lastIndexOf("/") + 1),
+            folderPath,
+            uploadStrategy,
+        );
+        setInputFolderPath("");
+        setMappings(await watchFolderService.getWatchMappings());
+    };
+
+    const handleRemoveWatchMapping = (mapping: WatchMapping) => {
+        watchFolderService
+            .mappingsAfterRemovingFolder(mapping.folderPath)
+            .then((ms) => setMappings(ms));
+    };
+
+    const closeChoiceModal = () => setChoiceModalOpen(false);
+
+    const uploadToSingleCollection = () => {
+        closeChoiceModal();
+        handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
+    };
+
+    const uploadToMultipleCollection = () => {
+        closeChoiceModal();
+        handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
+    };
+
+    return (
+        <>
+            <Dialog
+                open={open}
+                onClose={onClose}
+                PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
+            >
+                <DialogTitleWithCloseButton
+                    onClose={onClose}
+                    sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
+                >
+                    {t("WATCHED_FOLDERS")}
+                </DialogTitleWithCloseButton>
+                <DialogContent sx={{ flex: 1 }}>
+                    <Stack spacing={1} p={1.5} height={"100%"}>
+                        <MappingList
+                            mappings={mappings}
+                            handleRemoveWatchMapping={handleRemoveWatchMapping}
+                        />
+                        <Button
+                            fullWidth
+                            color="accent"
+                            onClick={handleAddFolderClick}
+                        >
+                            <span>+</span>
+                            <span
+                                style={{
+                                    marginLeft: "8px",
+                                }}
+                            ></span>
+                            {t("ADD_FOLDER")}
+                        </Button>
+                    </Stack>
+                </DialogContent>
+            </Dialog>
+            <UploadStrategyChoiceModal
+                open={choiceModalOpen}
+                onClose={closeChoiceModal}
+                uploadToSingleCollection={uploadToSingleCollection}
+                uploadToMultipleCollection={uploadToMultipleCollection}
+            />
+        </>
+    );
+};
+
+const MappingsContainer = styled(Box)(() => ({
+    height: "278px",
+    overflow: "auto",
+    "&::-webkit-scrollbar": {
+        width: "4px",
+    },
+}));
+
+const NoMappingsContainer = styled(VerticallyCentered)({
+    textAlign: "left",
+    alignItems: "flex-start",
+    marginBottom: "32px",
+});
+
+const EntryContainer = styled(Box)({
+    marginLeft: "12px",
+    marginRight: "6px",
+    marginBottom: "12px",
+});
+
+interface MappingListProps {
+    mappings: WatchMapping[];
+    handleRemoveWatchMapping: (value: WatchMapping) => void;
+}
+
+const MappingList: React.FC<MappingListProps> = ({
+    mappings,
+    handleRemoveWatchMapping,
+}) => {
+    return mappings.length === 0 ? (
+        <NoMappingsContent />
+    ) : (
+        <MappingsContainer>
+            {mappings.map((mapping) => {
+                return (
+                    <MappingEntry
+                        key={mapping.rootFolderName}
+                        mapping={mapping}
+                        handleRemoveMapping={handleRemoveWatchMapping}
+                    />
+                );
+            })}
+        </MappingsContainer>
+    );
+};
+
+const NoMappingsContent: React.FC = () => {
+    return (
+        <NoMappingsContainer>
+            <Stack spacing={1}>
+                <Typography variant="large" fontWeight={"bold"}>
+                    {t("NO_FOLDERS_ADDED")}
+                </Typography>
+                <Typography py={0.5} variant={"small"} color="text.muted">
+                    {t("FOLDERS_AUTOMATICALLY_MONITORED")}
+                </Typography>
+                <Typography variant={"small"} color="text.muted">
+                    <FlexWrapper gap={1}>
+                        <CheckmarkIcon />
+                        {t("UPLOAD_NEW_FILES_TO_ENTE")}
+                    </FlexWrapper>
+                </Typography>
+                <Typography variant={"small"} color="text.muted">
+                    <FlexWrapper gap={1}>
+                        <CheckmarkIcon />
+                        {t("REMOVE_DELETED_FILES_FROM_ENTE")}
+                    </FlexWrapper>
+                </Typography>
+            </Stack>
+        </NoMappingsContainer>
+    );
+};
+
+const CheckmarkIcon: React.FC = () => {
+    return (
+        <CheckIcon
+            fontSize="small"
+            sx={{
+                display: "inline",
+                fontSize: "15px",
+
+                color: (theme) => theme.palette.secondary.main,
+            }}
+        />
+    );
+};
+
+interface MappingEntryProps {
+    mapping: WatchMapping;
+    handleRemoveMapping: (mapping: WatchMapping) => void;
+}
+
+const MappingEntry: React.FC<MappingEntryProps> = ({
+    mapping,
+    handleRemoveMapping,
+}) => {
+    const appContext = React.useContext(AppContext);
+
+    const stopWatching = () => {
+        handleRemoveMapping(mapping);
+    };
+
+    const confirmStopWatching = () => {
+        appContext.setDialogMessage({
+            title: t("STOP_WATCHING_FOLDER"),
+            content: t("STOP_WATCHING_DIALOG_MESSAGE"),
+            close: {
+                text: t("CANCEL"),
+                variant: "secondary",
+            },
+            proceed: {
+                action: stopWatching,
+                text: t("YES_STOP"),
+                variant: "critical",
+            },
+        });
+    };
+
+    return (
+        <SpaceBetweenFlex>
+            <HorizontalFlex>
+                {mapping &&
+                mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
+                    <Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
+                        <FolderOpenIcon />
+                    </Tooltip>
+                ) : (
+                    <Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
+                        <FolderCopyOutlinedIcon />
+                    </Tooltip>
+                )}
+                <EntryContainer>
+                    <EntryHeading mapping={mapping} />
+                    <Typography color="text.muted" variant="small">
+                        {mapping.folderPath}
+                    </Typography>
+                </EntryContainer>
+            </HorizontalFlex>
+            <MappingEntryOptions confirmStopWatching={confirmStopWatching} />
+        </SpaceBetweenFlex>
+    );
+};
+
+interface EntryHeadingProps {
+    mapping: WatchMapping;
+}
+
+const EntryHeading: React.FC<EntryHeadingProps> = ({ mapping }) => {
+    const appContext = useContext(AppContext);
+    return (
+        <FlexWrapper gap={1}>
+            <Typography>{mapping.rootFolderName}</Typography>
+            {appContext.isFolderSyncRunning &&
+                watchFolderService.isMappingSyncInProgress(mapping) && (
+                    <CircularProgress size={12} />
+                )}
+        </FlexWrapper>
+    );
+};
+
+interface MappingEntryOptionsProps {
+    confirmStopWatching: () => void;
+}
+
+const MappingEntryOptions: React.FC<MappingEntryOptionsProps> = ({
+    confirmStopWatching,
+}) => {
+    return (
+        <OverflowMenu
+            menuPaperProps={{
+                sx: {
+                    backgroundColor: (theme) =>
+                        theme.colors.background.elevated2,
+                },
+            }}
+            ariaControls={"watch-mapping-option"}
+            triggerButtonIcon={<MoreHorizIcon />}
+        >
+            <OverflowMenuOption
+                color="critical"
+                onClick={confirmStopWatching}
+                startIcon={<DoNotDisturbOutlinedIcon />}
+            >
+                {t("STOP_WATCHING")}
+            </OverflowMenuOption>
+        </OverflowMenu>
+    );
+};

+ 0 - 152
web/apps/photos/src/components/WatchFolder/index.tsx

@@ -1,152 +0,0 @@
-import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
-import { Button, Dialog, DialogContent, Stack } from "@mui/material";
-import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
-import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
-import { t } from "i18next";
-import { AppContext } from "pages/_app";
-import { useContext, useEffect, useState } from "react";
-import watchFolderService from "services/watchFolder/watchFolderService";
-import { WatchMapping } from "types/watchFolder";
-import { getImportSuggestion } from "utils/upload";
-import { MappingList } from "./mappingList";
-
-interface Iprops {
-    open: boolean;
-    onClose: () => void;
-}
-
-export default function WatchFolder({ open, onClose }: Iprops) {
-    const [mappings, setMappings] = useState<WatchMapping[]>([]);
-    const [inputFolderPath, setInputFolderPath] = useState("");
-    const [choiceModalOpen, setChoiceModalOpen] = useState(false);
-    const appContext = useContext(AppContext);
-
-    const electron = globalThis.electron;
-
-    useEffect(() => {
-        if (!electron) return;
-        watchFolderService.getWatchMappings().then((m) => setMappings(m));
-    }, []);
-
-    useEffect(() => {
-        if (
-            appContext.watchFolderFiles &&
-            appContext.watchFolderFiles.length > 0
-        ) {
-            handleFolderDrop(appContext.watchFolderFiles);
-            appContext.setWatchFolderFiles(null);
-        }
-    }, [appContext.watchFolderFiles]);
-
-    const handleFolderDrop = async (folders: FileList) => {
-        for (let i = 0; i < folders.length; i++) {
-            const folder: any = folders[i];
-            const path = (folder.path as string).replace(/\\/g, "/");
-            if (await watchFolderService.isFolder(path)) {
-                await addFolderForWatching(path);
-            }
-        }
-    };
-
-    const addFolderForWatching = async (path: string) => {
-        if (!electron) return;
-
-        setInputFolderPath(path);
-        const files = await electron.getDirFiles(path);
-        const analysisResult = getImportSuggestion(
-            PICKED_UPLOAD_TYPE.FOLDERS,
-            files,
-        );
-        if (analysisResult.hasNestedFolders) {
-            setChoiceModalOpen(true);
-        } else {
-            handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
-        }
-    };
-
-    const handleAddFolderClick = async () => {
-        await handleFolderSelection();
-    };
-
-    const handleFolderSelection = async () => {
-        const folderPath = await watchFolderService.selectFolder();
-        if (folderPath) {
-            await addFolderForWatching(folderPath);
-        }
-    };
-
-    const handleAddWatchMapping = async (
-        uploadStrategy: UPLOAD_STRATEGY,
-        folderPath?: string,
-    ) => {
-        folderPath = folderPath || inputFolderPath;
-        await watchFolderService.addWatchMapping(
-            folderPath.substring(folderPath.lastIndexOf("/") + 1),
-            folderPath,
-            uploadStrategy,
-        );
-        setInputFolderPath("");
-        setMappings(await watchFolderService.getWatchMappings());
-    };
-
-    const handleRemoveWatchMapping = async (mapping: WatchMapping) => {
-        await watchFolderService.removeWatchMapping(mapping.folderPath);
-        setMappings(await watchFolderService.getWatchMappings());
-    };
-
-    const closeChoiceModal = () => setChoiceModalOpen(false);
-
-    const uploadToSingleCollection = () => {
-        closeChoiceModal();
-        handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
-    };
-
-    const uploadToMultipleCollection = () => {
-        closeChoiceModal();
-        handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
-    };
-
-    return (
-        <>
-            <Dialog
-                open={open}
-                onClose={onClose}
-                PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
-            >
-                <DialogTitleWithCloseButton
-                    onClose={onClose}
-                    sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
-                >
-                    {t("WATCHED_FOLDERS")}
-                </DialogTitleWithCloseButton>
-                <DialogContent sx={{ flex: 1 }}>
-                    <Stack spacing={1} p={1.5} height={"100%"}>
-                        <MappingList
-                            mappings={mappings}
-                            handleRemoveWatchMapping={handleRemoveWatchMapping}
-                        />
-                        <Button
-                            fullWidth
-                            color="accent"
-                            onClick={handleAddFolderClick}
-                        >
-                            <span>+</span>
-                            <span
-                                style={{
-                                    marginLeft: "8px",
-                                }}
-                            ></span>
-                            {t("ADD_FOLDER")}
-                        </Button>
-                    </Stack>
-                </DialogContent>
-            </Dialog>
-            <UploadStrategyChoiceModal
-                open={choiceModalOpen}
-                onClose={closeChoiceModal}
-                uploadToSingleCollection={uploadToSingleCollection}
-                uploadToMultipleCollection={uploadToMultipleCollection}
-            />
-        </>
-    );
-}

+ 0 - 23
web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx

@@ -1,23 +0,0 @@
-import { FlexWrapper } from "@ente/shared/components/Container";
-import { CircularProgress, Typography } from "@mui/material";
-import { AppContext } from "pages/_app";
-import { useContext } from "react";
-import watchFolderService from "services/watchFolder/watchFolderService";
-import { WatchMapping } from "types/watchFolder";
-
-interface Iprops {
-    mapping: WatchMapping;
-}
-
-export function EntryHeading({ mapping }: Iprops) {
-    const appContext = useContext(AppContext);
-    return (
-        <FlexWrapper gap={1}>
-            <Typography>{mapping.rootFolderName}</Typography>
-            {appContext.isFolderSyncRunning &&
-                watchFolderService.isMappingSyncInProgress(mapping) && (
-                    <CircularProgress size={12} />
-                )}
-        </FlexWrapper>
-    );
-}

+ 0 - 69
web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx

@@ -1,69 +0,0 @@
-import {
-    HorizontalFlex,
-    SpaceBetweenFlex,
-} from "@ente/shared/components/Container";
-import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
-import FolderOpenIcon from "@mui/icons-material/FolderOpen";
-import { Tooltip, Typography } from "@mui/material";
-import { t } from "i18next";
-import { AppContext } from "pages/_app";
-import React from "react";
-import { WatchMapping } from "types/watchFolder";
-import { EntryContainer } from "../styledComponents";
-
-import { UPLOAD_STRATEGY } from "constants/upload";
-import { EntryHeading } from "./entryHeading";
-import MappingEntryOptions from "./mappingEntryOptions";
-
-interface Iprops {
-    mapping: WatchMapping;
-    handleRemoveMapping: (mapping: WatchMapping) => void;
-}
-
-export function MappingEntry({ mapping, handleRemoveMapping }: Iprops) {
-    const appContext = React.useContext(AppContext);
-
-    const stopWatching = () => {
-        handleRemoveMapping(mapping);
-    };
-
-    const confirmStopWatching = () => {
-        appContext.setDialogMessage({
-            title: t("STOP_WATCHING_FOLDER"),
-            content: t("STOP_WATCHING_DIALOG_MESSAGE"),
-            close: {
-                text: t("CANCEL"),
-                variant: "secondary",
-            },
-            proceed: {
-                action: stopWatching,
-                text: t("YES_STOP"),
-                variant: "critical",
-            },
-        });
-    };
-
-    return (
-        <SpaceBetweenFlex>
-            <HorizontalFlex>
-                {mapping &&
-                mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
-                    <Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
-                        <FolderOpenIcon />
-                    </Tooltip>
-                ) : (
-                    <Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
-                        <FolderCopyOutlinedIcon />
-                    </Tooltip>
-                )}
-                <EntryContainer>
-                    <EntryHeading mapping={mapping} />
-                    <Typography color="text.muted" variant="small">
-                        {mapping.folderPath}
-                    </Typography>
-                </EntryContainer>
-            </HorizontalFlex>
-            <MappingEntryOptions confirmStopWatching={confirmStopWatching} />
-        </SpaceBetweenFlex>
-    );
-}

+ 0 - 33
web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx

@@ -1,33 +0,0 @@
-import { t } from "i18next";
-
-import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
-import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
-import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
-import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
-
-interface Iprops {
-    confirmStopWatching: () => void;
-}
-
-export default function MappingEntryOptions({ confirmStopWatching }: Iprops) {
-    return (
-        <OverflowMenu
-            menuPaperProps={{
-                sx: {
-                    backgroundColor: (theme) =>
-                        theme.colors.background.elevated2,
-                },
-            }}
-            ariaControls={"watch-mapping-option"}
-            triggerButtonIcon={<MoreHorizIcon />}
-        >
-            <OverflowMenuOption
-                color="critical"
-                onClick={confirmStopWatching}
-                startIcon={<DoNotDisturbOutlinedIcon />}
-            >
-                {t("STOP_WATCHING")}
-            </OverflowMenuOption>
-        </OverflowMenu>
-    );
-}

+ 0 - 26
web/apps/photos/src/components/WatchFolder/mappingList/index.tsx

@@ -1,26 +0,0 @@
-import { WatchMapping } from "types/watchFolder";
-import { MappingEntry } from "../mappingEntry";
-import { MappingsContainer } from "../styledComponents";
-import { NoMappingsContent } from "./noMappingsContent/noMappingsContent";
-interface Iprops {
-    mappings: WatchMapping[];
-    handleRemoveWatchMapping: (value: WatchMapping) => void;
-}
-
-export function MappingList({ mappings, handleRemoveWatchMapping }: Iprops) {
-    return mappings.length === 0 ? (
-        <NoMappingsContent />
-    ) : (
-        <MappingsContainer>
-            {mappings.map((mapping) => {
-                return (
-                    <MappingEntry
-                        key={mapping.rootFolderName}
-                        mapping={mapping}
-                        handleRemoveMapping={handleRemoveWatchMapping}
-                    />
-                );
-            })}
-        </MappingsContainer>
-    );
-}

+ 0 - 15
web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx

@@ -1,15 +0,0 @@
-import CheckIcon from "@mui/icons-material/Check";
-
-export function CheckmarkIcon() {
-    return (
-        <CheckIcon
-            fontSize="small"
-            sx={{
-                display: "inline",
-                fontSize: "15px",
-
-                color: (theme) => theme.palette.secondary.main,
-            }}
-        />
-    );
-}

+ 0 - 33
web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx

@@ -1,33 +0,0 @@
-import { Stack, Typography } from "@mui/material";
-import { t } from "i18next";
-
-import { FlexWrapper } from "@ente/shared/components/Container";
-import { NoMappingsContainer } from "../../styledComponents";
-import { CheckmarkIcon } from "./checkmarkIcon";
-
-export function NoMappingsContent() {
-    return (
-        <NoMappingsContainer>
-            <Stack spacing={1}>
-                <Typography variant="large" fontWeight={"bold"}>
-                    {t("NO_FOLDERS_ADDED")}
-                </Typography>
-                <Typography py={0.5} variant={"small"} color="text.muted">
-                    {t("FOLDERS_AUTOMATICALLY_MONITORED")}
-                </Typography>
-                <Typography variant={"small"} color="text.muted">
-                    <FlexWrapper gap={1}>
-                        <CheckmarkIcon />
-                        {t("UPLOAD_NEW_FILES_TO_ENTE")}
-                    </FlexWrapper>
-                </Typography>
-                <Typography variant={"small"} color="text.muted">
-                    <FlexWrapper gap={1}>
-                        <CheckmarkIcon />
-                        {t("REMOVE_DELETED_FILES_FROM_ENTE")}
-                    </FlexWrapper>
-                </Typography>
-            </Stack>
-        </NoMappingsContainer>
-    );
-}

+ 0 - 23
web/apps/photos/src/components/WatchFolder/styledComponents.tsx

@@ -1,23 +0,0 @@
-import { VerticallyCentered } from "@ente/shared/components/Container";
-import { Box } from "@mui/material";
-import { styled } from "@mui/material/styles";
-
-export const MappingsContainer = styled(Box)(() => ({
-    height: "278px",
-    overflow: "auto",
-    "&::-webkit-scrollbar": {
-        width: "4px",
-    },
-}));
-
-export const NoMappingsContainer = styled(VerticallyCentered)({
-    textAlign: "left",
-    alignItems: "flex-start",
-    marginBottom: "32px",
-});
-
-export const EntryContainer = styled(Box)({
-    marginLeft: "12px",
-    marginRight: "6px",
-    marginBottom: "12px",
-});

+ 1 - 1
web/apps/photos/src/services/upload/uploadManager.ts

@@ -14,7 +14,7 @@ import {
     getPublicCollectionUID,
 } from "services/publicCollectionService";
 import { getDisableCFUploadProxyFlag } from "services/userService";
-import watchFolderService from "services/watchFolder/watchFolderService";
+import watchFolderService from "services/watch";
 import { Collection } from "types/collection";
 import { EncryptedEnteFile, EnteFile } from "types/file";
 import { SetFiles } from "types/gallery";

+ 113 - 17
web/apps/photos/src/services/watchFolder/watchFolderService.ts → web/apps/photos/src/services/watch.ts

@@ -1,3 +1,8 @@
+/**
+ * @file Interface with the Node.js layer of our desktop app to provide the
+ * watch folders functionality.
+ */
+
 import { ensureElectron } from "@/next/electron";
 import log from "@/next/log";
 import { UPLOAD_RESULT, UPLOAD_STRATEGY } from "constants/upload";
@@ -12,17 +17,11 @@ import {
     WatchMappingSyncedFile,
 } from "types/watchFolder";
 import { groupFilesBasedOnCollectionID } from "utils/file";
-import { getValidFilesToUpload } from "utils/watch";
-import { removeFromCollection } from "../collectionService";
-import { getLocalFiles } from "../fileService";
-import { getParentFolderName } from "./utils";
-import {
-    diskFileAddedCallback,
-    diskFileRemovedCallback,
-    diskFolderRemovedCallback,
-} from "./watchFolderEventHandlers";
+import { isSystemFile } from "utils/upload";
+import { removeFromCollection } from "./collectionService";
+import { getLocalFiles } from "./fileService";
 
-class watchFolderService {
+class WatchFolderService {
     private eventQueue: EventQueueItem[] = [];
     private currentEvent: EventQueueItem;
     private currentlySyncedMapping: WatchMapping;
@@ -196,12 +195,9 @@ class watchFolderService {
         }
     }
 
-    async removeWatchMapping(folderPath: string) {
-        try {
-            await ensureElectron().removeWatchMapping(folderPath);
-        } catch (e) {
-            log.error("error while removing watch mapping", e);
-        }
+    async mappingsAfterRemovingFolder(folderPath: string) {
+        await ensureElectron().removeWatchMapping(folderPath);
+        return await this.getWatchMappings();
     }
 
     async getWatchMappings(): Promise<WatchMapping[]> {
@@ -641,4 +637,104 @@ class watchFolderService {
     }
 }
 
-export default new watchFolderService();
+const watchFolderService = new WatchFolderService();
+
+export default watchFolderService;
+
+const getParentFolderName = (filePath: string) => {
+    const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
+    const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
+    return folderName;
+};
+
+async function diskFileAddedCallback(file: ElectronFile) {
+    try {
+        const collectionNameAndFolderPath =
+            await watchFolderService.getCollectionNameAndFolderPath(file.path);
+
+        if (!collectionNameAndFolderPath) {
+            return;
+        }
+
+        const { collectionName, folderPath } = collectionNameAndFolderPath;
+
+        const event: EventQueueItem = {
+            type: "upload",
+            collectionName,
+            folderPath,
+            files: [file],
+        };
+        watchFolderService.pushEvent(event);
+        log.info(
+            `added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
+        );
+    } catch (e) {
+        log.error("error while calling diskFileAddedCallback", e);
+    }
+}
+
+async function diskFileRemovedCallback(filePath: string) {
+    try {
+        const collectionNameAndFolderPath =
+            await watchFolderService.getCollectionNameAndFolderPath(filePath);
+
+        if (!collectionNameAndFolderPath) {
+            return;
+        }
+
+        const { collectionName, folderPath } = collectionNameAndFolderPath;
+
+        const event: EventQueueItem = {
+            type: "trash",
+            collectionName,
+            folderPath,
+            paths: [filePath],
+        };
+        watchFolderService.pushEvent(event);
+        log.info(
+            `added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
+        );
+    } catch (e) {
+        log.error("error while calling diskFileRemovedCallback", e);
+    }
+}
+
+async function diskFolderRemovedCallback(folderPath: string) {
+    try {
+        const mappings = await watchFolderService.getWatchMappings();
+        const mapping = mappings.find(
+            (mapping) => mapping.folderPath === folderPath,
+        );
+        if (!mapping) {
+            log.info(`folder not found in mappings, ${folderPath}`);
+            throw Error(`Watch mapping not found`);
+        }
+        watchFolderService.pushTrashedDir(folderPath);
+        log.info(`added trashedDir, ${folderPath}`);
+    } catch (e) {
+        log.error("error while calling diskFolderRemovedCallback", e);
+    }
+}
+
+export function getValidFilesToUpload(
+    files: ElectronFile[],
+    mapping: WatchMapping,
+) {
+    const uniqueFilePaths = new Set<string>();
+    return files.filter((file) => {
+        if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
+            if (!uniqueFilePaths.has(file.path)) {
+                uniqueFilePaths.add(file.path);
+                return true;
+            }
+        }
+        return false;
+    });
+}
+
+function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
+    return (
+        mapping.ignoredFiles.includes(file.path) ||
+        mapping.syncedFiles.find((f) => f.path === file.path)
+    );
+}

+ 0 - 5
web/apps/photos/src/services/watchFolder/utils.ts

@@ -1,5 +0,0 @@
-export const getParentFolderName = (filePath: string) => {
-    const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
-    const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
-    return folderName;
-};

+ 0 - 73
web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts

@@ -1,73 +0,0 @@
-import log from "@/next/log";
-import { ElectronFile } from "types/upload";
-import { EventQueueItem } from "types/watchFolder";
-import watchFolderService from "./watchFolderService";
-
-export async function diskFileAddedCallback(file: ElectronFile) {
-    try {
-        const collectionNameAndFolderPath =
-            await watchFolderService.getCollectionNameAndFolderPath(file.path);
-
-        if (!collectionNameAndFolderPath) {
-            return;
-        }
-
-        const { collectionName, folderPath } = collectionNameAndFolderPath;
-
-        const event: EventQueueItem = {
-            type: "upload",
-            collectionName,
-            folderPath,
-            files: [file],
-        };
-        watchFolderService.pushEvent(event);
-        log.info(
-            `added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
-        );
-    } catch (e) {
-        log.error("error while calling diskFileAddedCallback", e);
-    }
-}
-
-export async function diskFileRemovedCallback(filePath: string) {
-    try {
-        const collectionNameAndFolderPath =
-            await watchFolderService.getCollectionNameAndFolderPath(filePath);
-
-        if (!collectionNameAndFolderPath) {
-            return;
-        }
-
-        const { collectionName, folderPath } = collectionNameAndFolderPath;
-
-        const event: EventQueueItem = {
-            type: "trash",
-            collectionName,
-            folderPath,
-            paths: [filePath],
-        };
-        watchFolderService.pushEvent(event);
-        log.info(
-            `added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
-        );
-    } catch (e) {
-        log.error("error while calling diskFileRemovedCallback", e);
-    }
-}
-
-export async function diskFolderRemovedCallback(folderPath: string) {
-    try {
-        const mappings = await watchFolderService.getWatchMappings();
-        const mapping = mappings.find(
-            (mapping) => mapping.folderPath === folderPath,
-        );
-        if (!mapping) {
-            log.info(`folder not found in mappings, ${folderPath}`);
-            throw Error(`Watch mapping not found`);
-        }
-        watchFolderService.pushTrashedDir(folderPath);
-        log.info(`added trashedDir, ${folderPath}`);
-    } catch (e) {
-        log.error("error while calling diskFolderRemovedCallback", e);
-    }
-}

+ 0 - 26
web/apps/photos/src/utils/watch/index.ts

@@ -1,26 +0,0 @@
-import { ElectronFile } from "types/upload";
-import { WatchMapping } from "types/watchFolder";
-import { isSystemFile } from "utils/upload";
-
-function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
-    return (
-        mapping.ignoredFiles.includes(file.path) ||
-        mapping.syncedFiles.find((f) => f.path === file.path)
-    );
-}
-
-export function getValidFilesToUpload(
-    files: ElectronFile[],
-    mapping: WatchMapping,
-) {
-    const uniqueFilePaths = new Set<string>();
-    return files.filter((file) => {
-        if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
-            if (!uniqueFilePaths.has(file.path)) {
-                uniqueFilePaths.add(file.path);
-                return true;
-            }
-        }
-        return false;
-    });
-}

+ 0 - 14
web/packages/next/types/file.ts

@@ -26,20 +26,6 @@ export interface DataStream {
     chunkCount: number;
 }
 
-export interface WatchMappingSyncedFile {
-    path: string;
-    uploadedFileID: number;
-    collectionID: number;
-}
-
-export interface WatchMapping {
-    rootFolderName: string;
-    folderPath: string;
-    uploadStrategy: UPLOAD_STRATEGY;
-    syncedFiles: WatchMappingSyncedFile[];
-    ignoredFiles: string[];
-}
-
 export interface EventQueueItem {
     type: "upload" | "trash";
     folderPath: string;

+ 31 - 4
web/packages/next/types/ipc.ts

@@ -3,7 +3,7 @@
 //
 // See [Note: types.ts <-> preload.ts <-> ipc.ts]
 
-import type { ElectronFile, WatchMapping } from "./file";
+import type { ElectronFile } from "./file";
 
 export interface AppUpdateInfo {
     autoUpdatable: boolean;
@@ -298,16 +298,16 @@ export interface Electron {
 
     removeWatchMapping: (folderPath: string) => Promise<void>;
 
-    getWatchMappings: () => Promise<WatchMapping[]>;
+    getWatchMappings: () => Promise<FolderWatch[]>;
 
     updateWatchMappingSyncedFiles: (
         folderPath: string,
-        files: WatchMapping["syncedFiles"],
+        files: FolderWatch["syncedFiles"],
     ) => Promise<void>;
 
     updateWatchMappingIgnoredFiles: (
         folderPath: string,
-        files: WatchMapping["ignoredFiles"],
+        files: FolderWatch["ignoredFiles"],
     ) => Promise<void>;
 
     // - FS legacy
@@ -332,3 +332,30 @@ export interface Electron {
     setToUploadCollection: (collectionName: string) => Promise<void>;
     getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
 }
+
+/**
+ * A top level folder that was selected by the user for watching.
+ *
+ * The user can set up multiple such watches. Each of these can in turn be
+ * syncing multiple on disk folders to one or more (dependening on the
+ * {@link uploadStrategy}) Ente albums.
+ *
+ * This type is passed across the IPC boundary. It is persisted on the Node.js
+ * side.
+ */
+export interface FolderWatch {
+    rootFolderName: string;
+    uploadStrategy: number;
+    folderPath: string;
+    syncedFiles: FolderWatchSyncedFile[];
+    ignoredFiles: string[];
+}
+
+/**
+ * An on-disk file that was synced as part of a folder watch.
+ */
+export interface FolderWatchSyncedFile {
+    path: string;
+    uploadedFileID: number;
+    collectionID: number;
+}