From 434c5eb453f1d9ed53c10539c12f9bdfd39b4a43 Mon Sep 17 00:00:00 2001 From: Florian Lauch <33784129+EdenComp@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:58:04 +0100 Subject: [PATCH] feat: Implement a soft delete with a bin (#106) Co-authored-by: Reza Rahemtola --- pages/login.tsx | 1 + src/components/DisplayCards.tsx | 20 ++++++- src/components/DriveCards.tsx | 20 ++++--- src/components/ResponsiveBar.tsx | 1 + src/components/SideBar.tsx | 10 ++++ src/components/file/DeleteBin.tsx | 84 +++++++++++++++++++++++++++++ src/components/file/DeleteFile.tsx | 34 ++++++++++-- src/components/file/RestoreFile.tsx | 65 ++++++++++++++++++++++ src/components/file/UploadFile.tsx | 1 + src/lib/contact.ts | 36 +++++++++++++ src/lib/drive.ts | 11 ++++ src/types/types.ts | 1 + 12 files changed, 272 insertions(+), 12 deletions(-) create mode 100644 src/components/file/DeleteBin.tsx create mode 100644 src/components/file/RestoreFile.tsx diff --git a/pages/login.tsx b/pages/login.tsx index eaae306d..d967b2fc 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -33,6 +33,7 @@ const Login = (): JSX.Element => { toast({ title: login.message, status: 'success' }); setUser(login.user); router.push('/dashboard'); + await login.user.drive.autoDelete() } else { toast({ title: login.message, status: 'error' }); } diff --git a/src/components/DisplayCards.tsx b/src/components/DisplayCards.tsx index d9f7a181..604191b2 100644 --- a/src/components/DisplayCards.tsx +++ b/src/components/DisplayCards.tsx @@ -10,6 +10,7 @@ import ProgramCards from 'components/ProgramCards'; import { useDriveContext } from 'contexts/drive'; import { useUserContext } from 'contexts/user'; +import DeleteBin from "./file/DeleteBin"; type CardsProps = { myPrograms: IPCProgram[]; @@ -60,7 +61,7 @@ const DisplayCards = ({ elem.path === path)} + files={files.filter((elem) => elem.path === path && !elem.deletedAt)} folders={folders.filter((elem) => elem.path === path)} /> @@ -107,6 +108,23 @@ const DisplayCards = ({ ); if (index === 4) return ; + if (index === 5) { + const deletedFiles = files.filter((elem) => elem.path === path && elem.deletedAt !== null) + const deletedFolders = folders.filter((elem) => elem.path === path) + + return ( + + + + Your bin + + + + + + ); + } + return ; }; diff --git a/src/components/DriveCards.tsx b/src/components/DriveCards.tsx index 7e1f39b6..3e1c2d02 100644 --- a/src/components/DriveCards.tsx +++ b/src/components/DriveCards.tsx @@ -31,6 +31,7 @@ import type { IPCFile, IPCFolder } from 'types/types'; import { useConfigContext } from 'contexts/config'; import { useDriveContext } from 'contexts/drive'; import { FcFile, FcFolder } from 'react-icons/fc'; +import RestoreFile from "./file/RestoreFile"; const PathCard = (): JSX.Element => { const { path, setPath } = useDriveContext(); @@ -140,12 +141,19 @@ const DriveCards = ({ files, folders }: DriveCardsProps): JSX.Element => { - - - - - - + <> + {file.deletedAt === null ? ( + <> + + + + + + + ) : + } + + diff --git a/src/components/ResponsiveBar.tsx b/src/components/ResponsiveBar.tsx index f9ee8f23..652f7acf 100644 --- a/src/components/ResponsiveBar.tsx +++ b/src/components/ResponsiveBar.tsx @@ -46,6 +46,7 @@ export const LeftBar = ({ myFilesTab="My files" myProgramsTab="My programs" profileTab="My profile" + binTab="Bin" configTab="Config" sharedFilesTab="Shared with me" setSelectedTab={setSelectedTab} diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index da3f1a55..785df508 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -28,6 +28,7 @@ type SideBarPropsType = { sharedFilesTab: string; myProgramsTab: string; profileTab: string; + binTab: string; configTab: string; deployButton: JSX.Element; setSelectedTab: (tab: number) => void; @@ -43,6 +44,7 @@ const SideBar = ({ myProgramsTab, profileTab, configTab, + binTab, deployButton, githubButton, setSelectedTab, @@ -139,6 +141,14 @@ const SideBar = ({ > {profileTab} + + {binTab} + { + const colorText = useColorModeValue('gray.800', 'white'); + const toast = useToast({ duration: 2000, isClosable: true }); + + const { user } = useUserContext(); + const { setFiles } = useDriveContext() + + const [isLoading, setIsLoading] = useState(false); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const deleteAllFiles = async () => { + setIsLoading(true); + if (user.account) { + const deleted = await user.drive.delete(files.map((file) => file.hash)); + if (deleted.success) { + const removed = await user.contact.deleteFiles(files.map((file) => file.id), concernedFiles); + + if (!removed.success) { + toast({ title: removed.message, status: 'error' }); + } else { + setFiles(user.drive.files); + toast( { title: "All the files in your bin have been deleted.", status: 'success' }) + } + } + } else { + toast({ title: 'Failed to load account', status: 'error' }); + } + setIsLoading(false); + onClose(); + } + + if (files.length === 0 && folders.length === 0) { + return ( + + Your bin is empty + + ) + } + + return ( + <> + + deleteAllFiles()} + isLoading={isLoading} + id="ipc-dashboard-delete-bin-button" + > + Delete + + } + > + Are you sure you want to delete all the files in your bin? + + + ) +} + +export default DeleteBin \ No newline at end of file diff --git a/src/components/file/DeleteFile.tsx b/src/components/file/DeleteFile.tsx index 18620a92..15ffaa06 100644 --- a/src/components/file/DeleteFile.tsx +++ b/src/components/file/DeleteFile.tsx @@ -16,7 +16,7 @@ type DeleteFileProps = { const DeleteFile = ({ file, concernedFiles }: DeleteFileProps): JSX.Element => { const { user } = useUserContext(); - const { setFiles } = useDriveContext(); + const { files, setFiles } = useDriveContext(); const toast = useToast({ duration: 2000, isClosable: true }); const { config } = useConfigContext(); const colorText = useColorModeValue('gray.800', 'white'); @@ -28,7 +28,6 @@ const DeleteFile = ({ file, concernedFiles }: DeleteFileProps): JSX.Element => { setIsLoading(true); if (user.account) { const deleted = await user.drive.delete([file.hash]); - toast({ title: deleted.message, status: deleted.success ? 'success' : 'error' }); if (deleted.success) { const removed = await user.contact.deleteFiles([file.id], concernedFiles); @@ -46,6 +45,31 @@ const DeleteFile = ({ file, concernedFiles }: DeleteFileProps): JSX.Element => { onClose(); }; + const moveToBin = async (deletedAt: number) => { + setIsLoading(true); + if (user.account) { + const moved = await user.contact.moveFileToBin(file, deletedAt, concernedFiles) + toast({ title: moved.message, status: moved.success ? 'success' : 'error' }); + + const index = files.indexOf(file); + if (index !== -1) { + files[index].deletedAt = deletedAt; + setFiles([...files]); + } + } else { + toast({ title: 'Failed to load account', status: 'error' }); + } + setIsLoading(false); + }; + + const onBinClicked = async () => { + if (!file.deletedAt) { + await moveToBin(Date.now()); + } else { + onOpen(); + } + } + if (!['owner', 'editor'].includes(file.permission)) return <>; return ( @@ -58,11 +82,11 @@ const DeleteFile = ({ file, concernedFiles }: DeleteFileProps): JSX.Element => { w="100%" p="0px" mx="4px" - onClick={() => onOpen()} - isLoading={false} + onClick={onBinClicked} + isLoading={isLoading} id="ipc-dashboard-delete-file-button" > - Delete + {file.deletedAt === null ? 'Move to bin' : 'Delete'} { + const { user } = useUserContext(); + const { files, setFiles } = useDriveContext(); + const toast = useToast({ duration: 2000, isClosable: true }); + const { config } = useConfigContext(); + const colorText = useColorModeValue('gray.800', 'white'); + + const [isLoading, setIsLoading] = useState(false); + + const restoreFile = async () => { + setIsLoading(true); + if (user.account) { + const moved = await user.contact.moveFileToBin(file, null, concernedFiles) + toast({ title: moved.message, status: moved.success ? 'success' : 'error' }); + + const index = files.indexOf(file); + if (index !== -1) { + files[index].deletedAt = null; + setFiles([...files]); + } + } else { + toast({ title: 'Failed to load account', status: 'error' }); + } + setIsLoading(false); + }; + + if (!['owner', 'editor'].includes(file.permission)) return <>; + + return ( + + + + + + + ); +}; + +export default DeleteFile; diff --git a/src/components/file/UploadFile.tsx b/src/components/file/UploadFile.tsx index 509fc1e0..348a074a 100644 --- a/src/components/file/UploadFile.tsx +++ b/src/components/file/UploadFile.tsx @@ -45,6 +45,7 @@ const UploadFile = (): JSX.Element => { createdAt: Date.now(), key: { iv: '', ephemPublicKey: '', ciphertext: '', mac: '' }, path, + deletedAt: null, permission: 'owner', }; diff --git a/src/lib/contact.ts b/src/lib/contact.ts index 28d20ca3..ef6ab64a 100644 --- a/src/lib/contact.ts +++ b/src/lib/contact.ts @@ -124,6 +124,8 @@ class Contact { } else if (type === 'delete') { const owner = this.contacts.find((co) => co.address === c.address)!; owner.files = owner.files.filter((f) => f.id !== fileId); + } else if (type === 'bin') { + foundFile.deletedAt = file.deletedAt; } } }); @@ -279,6 +281,40 @@ class Contact { } } + public async moveFileToBin(concernedFile: IPCFile, deletedAt: number | null, sharedFiles: IPCFile[]): Promise { + try { + let fileFound = false; + this.contacts.forEach(async (contact) => { + const file = contact.files.find((f) => f.id === concernedFile.id); + + if (file) { + file.deletedAt = deletedAt; + fileFound = true; + await this.publishAggregate(); + } + }); + if (!fileFound) { + const file = sharedFiles.find((f) => f.id === concernedFile.id); + if (!file) { + return { success: false, message: 'File not found' }; + } + await post.Publish({ + account: this.account!, + postType: 'InterPlanetaryCloud', + content: { file: { ...concernedFile, deletedAt }, tags: ['bin', concernedFile.id] }, + channel: 'TEST', + APIServer: DEFAULT_API_V2, + inlineRequested: true, + storageEngine: ItemType.ipfs, + }); + } + return { success: true, message: `File ${deletedAt === null ? "removed from" : "moved to"} the bin` }; + } catch (err) { + console.error(err); + return { success: false, message: `Failed to ${deletedAt === null ? "remove the file from" : "move the file to"} the bin` }; + } + } + public async addFileToContact(contactAddress: string, mainFile: IPCFile): Promise { try { if (this.account) { diff --git a/src/lib/drive.ts b/src/lib/drive.ts index f3d963cb..99e7c547 100644 --- a/src/lib/drive.ts +++ b/src/lib/drive.ts @@ -11,6 +11,8 @@ import type { AggregateType, IPCContact, IPCFile, IPCFolder, ResponseType, Uploa import ArraybufferToString from 'utils/arraybufferToString'; +export const MONTH_MILLIS = 86400 * 30 * 1000 + class Drive { public files: IPCFile[]; @@ -93,6 +95,15 @@ class Drive { } } + public async autoDelete() { + try { + const filesToDelete = this.files.filter((file) => file.deletedAt !== null && Date.now() - file.deletedAt >= MONTH_MILLIS) + await this.delete(filesToDelete.map((file) => file.hash)) + } catch (err) { + console.error(err) + } + } + public async delete(fileHashes: string[]): Promise { try { if (this.account) { diff --git a/src/types/types.ts b/src/types/types.ts index a57c8d10..3d1d10a7 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -9,6 +9,7 @@ export type IPCFile = { createdAt: number; path: string; permission: IPCPermission; + deletedAt: number | null; }; export type IPCFolder = {