import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; import { MENU_MARGIN_FROM_APP_BORDER } from '@/constants'; import { Disclosure, DisclosureButton, DisclosurePanel, } from '@reach/disclosure'; import VisuallyHidden from '@reach/visually-hidden'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { Icon } from '../Icon'; import { useCloseOnClickOutside } from '../utils'; import { ChallengeReason, ContentType, SNFile } from '@standardnotes/snjs'; import { confirmDialog } from '@/services/alertService'; import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'; import { parseFileName } from '@standardnotes/filepicker'; import { PopoverFileItemAction, PopoverFileItemActionType, } from './PopoverFileItemAction'; import { PopoverDragNDropWrapper } from './PopoverDragNDropWrapper'; type Props = { application: WebApplication; appState: AppState; onClickPreprocessing?: () => Promise; }; export const AttachedFilesButton: FunctionComponent = observer( ({ application, appState, onClickPreprocessing }) => { const note = Object.values(appState.notes.selectedNotes)[0]; const [open, setOpen] = useState(false); const [position, setPosition] = useState({ top: 0, right: 0, }); const [maxHeight, setMaxHeight] = useState('auto'); const buttonRef = useRef(null); const panelRef = useRef(null); const containerRef = useRef(null); useCloseOnClickOutside(containerRef, () => { setOpen(false); }); const [attachedFilesCount, setAttachedFilesCount] = useState( note ? application.items.getFilesForNote(note).length : 0 ); const reloadAttachedFilesCount = useCallback(() => { setAttachedFilesCount( note ? application.items.getFilesForNote(note).length : 0 ); }, [application.items, note]); useEffect(() => { const unregisterFileStream = application.streamItems( ContentType.File, () => { reloadAttachedFilesCount(); } ); return () => { unregisterFileStream(); }; }, [application, reloadAttachedFilesCount]); const toggleAttachedFilesMenu = async () => { const rect = buttonRef.current?.getBoundingClientRect(); if (rect) { const { clientHeight } = document.documentElement; const footerElementRect = document .getElementById('footer-bar') ?.getBoundingClientRect(); const footerHeightInPx = footerElementRect?.height; if (footerHeightInPx) { setMaxHeight( clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER ); } setPosition({ top: rect.bottom, right: document.body.clientWidth - rect.right, }); const newOpenState = !open; if (newOpenState && onClickPreprocessing) { await onClickPreprocessing(); } setOpen(newOpenState); } }; const deleteFile = async (file: SNFile) => { const shouldDelete = await confirmDialog({ text: `Are you sure you want to permanently delete "${file.nameWithExt}"?`, confirmButtonStyle: 'danger', }); if (shouldDelete) { const deletingToastId = addToast({ type: ToastType.Loading, message: `Deleting file "${file.nameWithExt}"...`, }); await application.deleteItem(file); addToast({ type: ToastType.Success, message: `Deleted file "${file.nameWithExt}"`, }); dismissToast(deletingToastId); } }; const downloadFile = async (file: SNFile) => { appState.files.downloadFile(file); }; const attachFileToNote = async (file: SNFile) => { await application.items.associateFileWithNote(file, note); }; const detachFileFromNote = async (file: SNFile) => { await application.items.disassociateFileWithNote(file, note); }; const toggleFileProtection = async (file: SNFile) => { let result: SNFile | undefined; if (file.protected) { result = await application.protections.unprotectFile(file); } else { result = await application.protections.protectFile(file); } const isProtected = result ? result.protected : file.protected; return isProtected; }; const authorizeProtectedActionForFile = async ( file: SNFile, challengeReason: ChallengeReason ) => { const authorizedFiles = await application.protections.authorizeProtectedActionForFiles( [file], challengeReason ); const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file); return isAuthorized; }; const renameFile = async (file: SNFile, fileName: string) => { const { name, ext } = parseFileName(fileName); await application.items.renameFile(file, name, ext); }; const handleFileAction = async (action: PopoverFileItemAction) => { const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file; let isAuthorizedForAction = true; if ( file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection ) { isAuthorizedForAction = await authorizeProtectedActionForFile( file, ChallengeReason.AccessProtectedFile ); } if (!isAuthorizedForAction) { return false; } switch (action.type) { case PopoverFileItemActionType.AttachFileToNote: await attachFileToNote(file); break; case PopoverFileItemActionType.DetachFileToNote: await detachFileFromNote(file); break; case PopoverFileItemActionType.DeleteFile: await deleteFile(file); break; case PopoverFileItemActionType.DownloadFile: await downloadFile(file); break; case PopoverFileItemActionType.ToggleFileProtection: { const isProtected = await toggleFileProtection(file); action.callback(isProtected); break; } case PopoverFileItemActionType.RenameFile: await renameFile(file, action.payload.name); break; } application.sync.sync(); return true; }; return (
{ if (event.key === 'Escape') { setOpen(false); } }} ref={buttonRef} className={`sn-icon-button border-contrast ${ attachedFilesCount > 0 ? 'py-1 px-3' : '' }`} > Attached files {attachedFilesCount > 0 && ( {attachedFilesCount} )} { if (event.key === 'Escape') { setOpen(false); buttonRef.current?.focus(); } }} ref={panelRef} style={{ ...position, maxHeight, }} className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed" > {open && ( )}
); } );