From b31afee10819a6beffd060039eb559ee09895c16 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 10 Mar 2022 13:51:28 +0530 Subject: [PATCH] feat: add files popover in note toolbar (#913) --- .../components/ApplicationView.tsx | 33 --- .../AttachedFilesButton.tsx | 260 ++++++++++++++++++ .../AttachedFilesPopover.tsx | 195 +++++++++++++ .../PopoverDragNDropWrapper.tsx | 143 ++++++++++ .../AttachedFilesPopover/PopoverFileItem.tsx | 129 +++++++++ .../PopoverFileItemAction.tsx | 32 +++ .../PopoverFileSubmenu.tsx | 188 +++++++++++++ app/assets/javascripts/components/Icon.tsx | 28 +- .../components/NoteView/NoteView.tsx | 28 ++ .../components/NotesOptions/AddTagOption.tsx | 2 - app/assets/javascripts/components/utils.ts | 9 +- app/assets/javascripts/constants.ts | 4 +- .../ui_models/app_state/app_state.ts | 3 + .../ui_models/app_state/files_state.ts | 142 ++++++++++ app/assets/javascripts/utils/index.ts | 6 +- app/assets/stylesheets/_sn.scss | 38 ++- package.json | 12 +- yarn.lock | 122 ++++---- 18 files changed, 1269 insertions(+), 105 deletions(-) create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesButton.tsx create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/PopoverDragNDropWrapper.tsx create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItemAction.tsx create mode 100644 app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx create mode 100644 app/assets/javascripts/ui_models/app_state/files_state.ts diff --git a/app/assets/javascripts/components/ApplicationView.tsx b/app/assets/javascripts/components/ApplicationView.tsx index 2cfc18f39..6210d8da1 100644 --- a/app/assets/javascripts/components/ApplicationView.tsx +++ b/app/assets/javascripts/components/ApplicationView.tsx @@ -8,7 +8,6 @@ import { removeFromArray, } from '@standardnotes/snjs'; import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/constants'; -import { STRING_DEFAULT_FILE_ERROR } from '@/strings'; import { alertDialog } from '@/services/alertService'; import { WebAppEvent, WebApplication } from '@/ui_models/application'; import { PureComponent } from '@/components/Abstract/PureComponent'; @@ -51,17 +50,10 @@ export class ApplicationView extends PureComponent { appClass: '', challenges: [], }; - this.onDragDrop = this.onDragDrop.bind(this); - this.onDragOver = this.onDragOver.bind(this); - this.addDragDropHandlers(); } deinit() { (this.application as unknown) = undefined; - window.removeEventListener('dragover', this.onDragOver, true); - window.removeEventListener('drop', this.onDragDrop, true); - (this.onDragDrop as unknown) = undefined; - (this.onDragOver as unknown) = undefined; super.deinit(); } @@ -150,31 +142,6 @@ export class ApplicationView extends PureComponent { } } - addDragDropHandlers() { - /** - * Disable dragging and dropping of files (but allow text) into main SN interface. - * both 'dragover' and 'drop' are required to prevent dropping of files. - * This will not prevent extensions from receiving drop events. - */ - window.addEventListener('dragover', this.onDragOver, true); - window.addEventListener('drop', this.onDragDrop, true); - } - - onDragOver(event: DragEvent) { - if (event.dataTransfer?.files.length) { - event.preventDefault(); - } - } - - onDragDrop(event: DragEvent) { - if (event.dataTransfer?.files.length) { - event.preventDefault(); - void alertDialog({ - text: STRING_DEFAULT_FILE_ERROR, - }); - } - } - async handleDemoSignInFromParams() { if ( window.location.href.includes('demo') && diff --git a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesButton.tsx new file mode 100644 index 000000000..0a21ccea8 --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -0,0 +1,260 @@ +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 && ( + + )} + + +
+ ); + } +); diff --git a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx new file mode 100644 index 000000000..996d3d20d --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -0,0 +1,195 @@ +import { ContentType, SNFile } from '@standardnotes/snjs'; +import { FilesIllustration } from '@standardnotes/stylekit'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { StateUpdater, useCallback, useEffect, useState } from 'preact/hooks'; +import { Button } from '../Button'; +import { Icon } from '../Icon'; +import { PopoverTabs, PopoverWrapperProps } from './PopoverDragNDropWrapper'; +import { PopoverFileItem } from './PopoverFileItem'; +import { PopoverFileItemActionType } from './PopoverFileItemAction'; + +type Props = PopoverWrapperProps & { + currentTab: PopoverTabs; + setCurrentTab: StateUpdater; +}; + +export const AttachedFilesPopover: FunctionComponent = observer( + ({ + application, + appState, + note, + fileActionHandler, + currentTab, + setCurrentTab, + }) => { + const [attachedFiles, setAttachedFiles] = useState([]); + const [allFiles, setAllFiles] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + + const filesList = + currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles; + + const filteredList = + searchQuery.length > 0 + ? filesList.filter( + (file) => file.nameWithExt.toLowerCase().indexOf(searchQuery) !== -1 + ) + : filesList; + + const reloadAttachedFiles = useCallback(() => { + setAttachedFiles( + application.items + .getFilesForNote(note) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) + ); + }, [application.items, note]); + + const reloadAllFiles = useCallback(() => { + setAllFiles( + application + .getItems(ContentType.File) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) as SNFile[] + ); + }, [application]); + + useEffect(() => { + const unregisterFileStream = application.streamItems( + ContentType.File, + () => { + reloadAttachedFiles(); + reloadAllFiles(); + } + ); + + return () => { + unregisterFileStream(); + }; + }, [application, reloadAllFiles, reloadAttachedFiles]); + + const handleAttachFilesClick = async () => { + const uploadedFiles = await appState.files.uploadNewFile(); + if (!uploadedFiles) { + return; + } + if (currentTab === PopoverTabs.AttachedFiles) { + uploadedFiles.forEach((file) => { + fileActionHandler({ + type: PopoverFileItemActionType.AttachFileToNote, + payload: file, + }); + }); + } + }; + + return ( +
+
+ + +
+
+ {filteredList.length > 0 || searchQuery.length > 0 ? ( +
+
+ { + setSearchQuery((e.target as HTMLInputElement).value); + }} + /> + {searchQuery.length > 0 && ( + + )} +
+
+ ) : null} + {filteredList.length > 0 ? ( + filteredList.map((file: SNFile) => { + return ( + + ); + }) + ) : ( +
+
+ +
+
+ {searchQuery.length > 0 + ? 'No result found' + : currentTab === PopoverTabs.AttachedFiles + ? 'No files attached to this note' + : 'No files found in this account'} +
+ +
+ Or drop your files here +
+
+ )} +
+ {filteredList.length > 0 && ( + + )} +
+ ); + } +); diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverDragNDropWrapper.tsx b/app/assets/javascripts/components/AttachedFilesPopover/PopoverDragNDropWrapper.tsx new file mode 100644 index 000000000..487b4e61e --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/PopoverDragNDropWrapper.tsx @@ -0,0 +1,143 @@ +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { StreamingFileReader } from '@standardnotes/filepicker'; +import { SNNote } from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { AttachedFilesPopover } from './AttachedFilesPopover'; +import { + PopoverFileItemAction, + PopoverFileItemActionType, +} from './PopoverFileItemAction'; + +export enum PopoverTabs { + AttachedFiles, + AllFiles, +} + +export type PopoverWrapperProps = { + application: WebApplication; + appState: AppState; + note: SNNote; + fileActionHandler: (action: PopoverFileItemAction) => Promise; +}; + +export const PopoverDragNDropWrapper: FunctionComponent< + PopoverWrapperProps +> = ({ fileActionHandler, appState, application, note }) => { + const dropzoneRef = useRef(null); + + const [isDragging, setIsDragging] = useState(false); + const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles); + const dragCounter = useRef(0); + + const handleDrag = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const handleDragIn = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragCounter.current = dragCounter.current + 1; + + if (event.dataTransfer?.items.length) { + setIsDragging(true); + } + }; + + const handleDragOut = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + dragCounter.current = dragCounter.current - 1; + + if (dragCounter.current > 0) { + return; + } + + setIsDragging(false); + }; + + const handleDrop = useCallback( + (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + + setIsDragging(false); + + if (event.dataTransfer?.items.length) { + Array.from(event.dataTransfer.items).forEach(async (item) => { + let fileOrHandle; + if (StreamingFileReader.available()) { + fileOrHandle = + (await item.getAsFileSystemHandle()) as FileSystemFileHandle; + } else { + fileOrHandle = item.getAsFile(); + } + if (fileOrHandle) { + const uploadedFiles = await appState.files.uploadNewFile( + fileOrHandle + ); + if (!uploadedFiles) { + return; + } + + if (currentTab === PopoverTabs.AttachedFiles) { + uploadedFiles.forEach((file) => { + fileActionHandler({ + type: PopoverFileItemActionType.AttachFileToNote, + payload: file, + }); + }); + } + } + }); + + event.dataTransfer.clearData(); + dragCounter.current = 0; + } + }, + [appState.files, currentTab, fileActionHandler] + ); + + useEffect(() => { + const dropzoneElement = dropzoneRef.current; + + if (dropzoneElement) { + dropzoneElement.addEventListener('dragenter', handleDragIn); + dropzoneElement.addEventListener('dragleave', handleDragOut); + dropzoneElement.addEventListener('dragover', handleDrag); + dropzoneElement.addEventListener('drop', handleDrop); + } + + return () => { + dropzoneElement?.removeEventListener('dragenter', handleDragIn); + dropzoneElement?.removeEventListener('dragleave', handleDragOut); + dropzoneElement?.removeEventListener('dragover', handleDrag); + dropzoneElement?.removeEventListener('drop', handleDrop); + }; + }, [handleDrop]); + + return ( +
+ +
+ ); +}; diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx new file mode 100644 index 000000000..dee6cb335 --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx @@ -0,0 +1,129 @@ +import { KeyboardKey } from '@/services/ioService'; +import { formatSizeToReadableString } from '@standardnotes/filepicker'; +import { SNFile } from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { ICONS } from '../Icon'; +import { + PopoverFileItemAction, + PopoverFileItemActionType, +} from './PopoverFileItemAction'; +import { PopoverFileSubmenu } from './PopoverFileSubmenu'; + +const getIconForFileType = (fileType: string) => { + let iconType = 'file-other'; + + if (fileType === 'pdf') { + iconType = 'file-pdf'; + } + + if (/^(docx?|odt)/.test(fileType)) { + iconType = 'file-doc'; + } + + if (/^pptx?/.test(fileType)) { + iconType = 'file-ppt'; + } + + if (/^(xlsx?|ods)/.test(fileType)) { + iconType = 'file-xls'; + } + + if (/^(jpe?g|a?png|webp|gif)/.test(fileType)) { + iconType = 'file-image'; + } + + if (/^(mov|mp4|mkv)/.test(fileType)) { + iconType = 'file-mov'; + } + + if (/^(wav|mp3|flac|ogg)/.test(fileType)) { + iconType = 'file-music'; + } + + if (/^(zip|rar|7z)/.test(fileType)) { + iconType = 'file-zip'; + } + + const IconComponent = ICONS[iconType as keyof typeof ICONS]; + + return ; +}; + +export type PopoverFileItemProps = { + file: SNFile; + isAttachedToNote: boolean; + handleFileAction: (action: PopoverFileItemAction) => Promise; +}; + +export const PopoverFileItem: FunctionComponent = ({ + file, + isAttachedToNote, + handleFileAction, +}) => { + const [fileName, setFileName] = useState(file.nameWithExt); + const [isRenamingFile, setIsRenamingFile] = useState(false); + const fileNameInputRef = useRef(null); + + useEffect(() => { + if (isRenamingFile) { + fileNameInputRef.current?.focus(); + } + }, [isRenamingFile]); + + const renameFile = async (file: SNFile, name: string) => { + const didRename = await handleFileAction({ + type: PopoverFileItemActionType.RenameFile, + payload: { + file, + name, + }, + }); + if (didRename) { + setIsRenamingFile(false); + } + }; + + const handleFileNameInput = (event: Event) => { + setFileName((event.target as HTMLInputElement).value); + }; + + const handleFileNameInputKeyDown = (event: KeyboardEvent) => { + if (event.key === KeyboardKey.Enter) { + renameFile(file, fileName); + return; + } + }; + + return ( +
+
+ {getIconForFileType(file.ext ?? '')} +
+ {isRenamingFile ? ( + + ) : ( +
{file.nameWithExt}
+ )} +
+ {file.created_at.toLocaleString()} ยท{' '} + {formatSizeToReadableString(file.size)} +
+
+
+ +
+ ); +}; diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItemAction.tsx b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItemAction.tsx new file mode 100644 index 000000000..dd10b01ef --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItemAction.tsx @@ -0,0 +1,32 @@ +import { SNFile } from '@standardnotes/snjs'; + +export enum PopoverFileItemActionType { + AttachFileToNote, + DetachFileToNote, + DeleteFile, + DownloadFile, + RenameFile, + ToggleFileProtection, +} + +export type PopoverFileItemAction = + | { + type: Exclude< + PopoverFileItemActionType, + | PopoverFileItemActionType.RenameFile + | PopoverFileItemActionType.ToggleFileProtection + >; + payload: SNFile; + } + | { + type: PopoverFileItemActionType.ToggleFileProtection; + payload: SNFile; + callback: (isProtected: boolean) => void; + } + | { + type: PopoverFileItemActionType.RenameFile; + payload: { + file: SNFile; + name: string; + }; + }; diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx new file mode 100644 index 000000000..f585b2268 --- /dev/null +++ b/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -0,0 +1,188 @@ +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; +import { + calculateSubmenuStyle, + SubmenuStyle, +} from '@/utils/calculateSubmenuStyle'; +import { + Disclosure, + DisclosureButton, + DisclosurePanel, +} from '@reach/disclosure'; +import { FunctionComponent } from 'preact'; +import { + StateUpdater, + useCallback, + useEffect, + useRef, + useState, +} from 'preact/hooks'; +import { Icon } from '../Icon'; +import { Switch } from '../Switch'; +import { useCloseOnBlur } from '../utils'; +import { PopoverFileItemProps } from './PopoverFileItem'; +import { PopoverFileItemActionType } from './PopoverFileItemAction'; + +type Props = Omit & { + setIsRenamingFile: StateUpdater; +}; + +export const PopoverFileSubmenu: FunctionComponent = ({ + file, + isAttachedToNote, + handleFileAction, + setIsRenamingFile, +}) => { + const menuContainerRef = useRef(null); + const menuButtonRef = useRef(null); + const menuRef = useRef(null); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isFileProtected, setIsFileProtected] = useState(file.protected); + const [menuStyle, setMenuStyle] = useState({ + right: 0, + bottom: 0, + maxHeight: 'auto', + }); + const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen); + + const closeMenu = () => { + setIsMenuOpen(false); + }; + + const toggleMenu = () => { + if (!isMenuOpen) { + const menuPosition = calculateSubmenuStyle(menuButtonRef.current); + if (menuPosition) { + setMenuStyle(menuPosition); + } + } + + setIsMenuOpen(!isMenuOpen); + }; + + const recalculateMenuStyle = useCallback(() => { + const newMenuPosition = calculateSubmenuStyle( + menuButtonRef.current, + menuRef.current + ); + + if (newMenuPosition) { + setMenuStyle(newMenuPosition); + } + }, []); + + useEffect(() => { + if (isMenuOpen) { + setTimeout(() => { + recalculateMenuStyle(); + }); + } + }, [isMenuOpen, recalculateMenuStyle]); + + return ( +
+ + + + + + {isMenuOpen && ( + <> + {isAttachedToNote ? ( + + ) : ( + + )} +
+ +
+ + + + )} +
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 196a63e45..b7c24f52b 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -9,6 +9,7 @@ import { ArrowLeftIcon, ArrowsSortDownIcon, ArrowsSortUpIcon, + AttachmentFileIcon, AuthenticatorIcon, CheckBoldIcon, CheckCircleIcon, @@ -17,6 +18,7 @@ import { ChevronRightIcon, CloseIcon, CloudOffIcon, + ClearCircleFilledIcon, CodeIcon, CopyIcon, DashboardIcon, @@ -25,12 +27,22 @@ import { EmailIcon, EyeIcon, EyeOffIcon, + FileDocIcon, + FileImageIcon, + FileMovIcon, + FileMusicIcon, + FileOtherIcon, + FilePdfIcon, + FilePptIcon, + FileXlsIcon, + FileZipIcon, HashtagIcon, HashtagOffIcon, HelpIcon, HistoryIcon, InfoIcon, KeyboardIcon, + LinkIcon, LinkOffIcon, ListBulleted, ListedIcon, @@ -44,9 +56,9 @@ import { MoreIcon, NotesIcon, PasswordIcon, - PencilOffIcon, PencilFilledIcon, PencilIcon, + PencilOffIcon, PinFilledIcon, PinIcon, PlainTextIcon, @@ -75,17 +87,28 @@ import { WindowIcon, } from '@standardnotes/stylekit'; -const ICONS = { +export const ICONS = { 'account-circle': AccountCircleIcon, 'arrow-left': ArrowLeftIcon, 'arrows-sort-down': ArrowsSortDownIcon, 'arrows-sort-up': ArrowsSortUpIcon, + 'attachment-file': AttachmentFileIcon, 'check-bold': CheckBoldIcon, 'check-circle': CheckCircleIcon, 'chevron-down': ChevronDownIcon, 'chevron-right': ChevronRightIcon, 'cloud-off': CloudOffIcon, + 'clear-circle-filled': ClearCircleFilledIcon, 'eye-off': EyeOffIcon, + 'file-doc': FileDocIcon, + 'file-image': FileImageIcon, + 'file-mov': FileMovIcon, + 'file-music': FileMusicIcon, + 'file-other': FileOtherIcon, + 'file-pdf': FilePdfIcon, + 'file-ppt': FilePptIcon, + 'file-xls': FileXlsIcon, + 'file-zip': FileZipIcon, 'hashtag-off': HashtagOffIcon, 'link-off': LinkOffIcon, 'list-bulleted': ListBulleted, @@ -121,6 +144,7 @@ const ICONS = { history: HistoryIcon, info: InfoIcon, keyboard: KeyboardIcon, + link: LinkIcon, listed: ListedIcon, lock: LockIcon, markdown: MarkdownIcon, diff --git a/app/assets/javascripts/components/NoteView/NoteView.tsx b/app/assets/javascripts/components/NoteView/NoteView.tsx index 4de1ad6a9..33d8f21bb 100644 --- a/app/assets/javascripts/components/NoteView/NoteView.tsx +++ b/app/assets/javascripts/components/NoteView/NoteView.tsx @@ -17,6 +17,8 @@ import { ItemMutator, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, NoteViewController, + FeatureIdentifier, + FeatureStatus, } from '@standardnotes/snjs'; import { debounce, isDesktopApplication } from '@/utils'; import { KeyboardModifier, KeyboardKey } from '@/services/ioService'; @@ -37,6 +39,7 @@ import { ComponentView } from '../ComponentView'; import { PanelSide, PanelResizer, PanelResizeType } from '../PanelResizer'; import { ElementIds } from '@/element_ids'; import { ChangeEditorButton } from '../ChangeEditorButton'; +import { AttachedFilesButton } from '../AttachedFilesPopover/AttachedFilesButton'; const MINIMUM_STATUS_DURATION = 400; const TEXTAREA_DEBOUNCE = 100; @@ -100,6 +103,7 @@ type State = { editorTitle: string; editorText: string; isDesktop?: boolean; + isEntitledToFiles: boolean; lockText: string; marginResizersEnabled?: boolean; monospaceFont?: boolean; @@ -168,6 +172,9 @@ export class NoteView extends PureComponent { editorText: '', editorTitle: '', isDesktop: isDesktopApplication(), + isEntitledToFiles: + this.application.features.getFeatureStatus(FeatureIdentifier.Files) === + FeatureStatus.Entitled, lockText: 'Note Editing Disabled', noteStatus: undefined, noteLocked: this.controller.note.locked, @@ -321,6 +328,15 @@ export class NoteView extends PureComponent { /** @override */ async onAppEvent(eventName: ApplicationEvent) { switch (eventName) { + case ApplicationEvent.FeaturesUpdated: + case ApplicationEvent.UserRolesChanged: + this.setState({ + isEntitledToFiles: + this.application.features.getFeatureStatus( + FeatureIdentifier.Files + ) === FeatureStatus.Entitled, + }); + break; case ApplicationEvent.PreferencesChanged: this.reloadPreferences(); break; @@ -1027,6 +1043,18 @@ export class NoteView extends PureComponent { )} + {this.state.isEntitledToFiles && + window.enabledUnfinishedFeatures && ( +
+ +
+ )}
= observer( const menuPosition = calculateSubmenuStyle(menuButtonRef.current); if (menuPosition) { setMenuStyle(menuPosition); - console.log(menuPosition); } } @@ -53,7 +52,6 @@ export const AddTagOption: FunctionComponent = observer( if (newMenuPosition) { setMenuStyle(newMenuPosition); - console.log(newMenuPosition); } }, []); diff --git a/app/assets/javascripts/components/utils.ts b/app/assets/javascripts/components/utils.ts index 4fc543b8f..33f04f67e 100644 --- a/app/assets/javascripts/components/utils.ts +++ b/app/assets/javascripts/components/utils.ts @@ -46,8 +46,13 @@ export function useCloseOnClickOutside( if (!container.current) { return; } - const isDescendant = container.current.contains(event.target as Node); - if (!isDescendant) { + const isDescendantOfContainer = container.current.contains( + event.target as Node + ); + const isDescendantOfDialog = (event.target as HTMLElement).closest( + '[role="dialog"]' + ); + if (!isDescendantOfContainer && !isDescendantOfDialog) { callback(); } }, diff --git a/app/assets/javascripts/constants.ts b/app/assets/javascripts/constants.ts index 19a24688d..3e18aa7c7 100644 --- a/app/assets/javascripts/constants.ts +++ b/app/assets/javascripts/constants.ts @@ -13,4 +13,6 @@ export const NOTES_LIST_SCROLL_THRESHOLD = 200; export const MILLISECONDS_IN_A_DAY = 1000 * 60 * 60 * 24; export const DAYS_IN_A_WEEK = 7; export const DAYS_IN_A_YEAR = 365; -export const BYTES_IN_ONE_MEGABYTE = 1000000; + +export const BYTES_IN_ONE_KILOBYTE = 1_000; +export const BYTES_IN_ONE_MEGABYTE = 1_000_000; diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index ecd81c7af..f67fe522f 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -26,6 +26,7 @@ import { } from 'mobx'; import { ActionsMenuState } from './actions_menu_state'; import { FeaturesState } from './features_state'; +import { FilesState } from './files_state'; import { NotesState } from './notes_state'; import { NotesViewState } from './notes_view_state'; import { NoteTagsState } from './note_tags_state'; @@ -89,6 +90,7 @@ export class AppState { readonly tags: TagsState; readonly notesView: NotesViewState; readonly subscription: SubscriptionState; + readonly files: FilesState; isSessionsModalVisible = false; @@ -139,6 +141,7 @@ export class AppState { this, this.appEventObserverRemovers ); + this.files = new FilesState(application); this.addAppEventObserver(); this.streamNotesAndTags(); this.onVisibilityChange = () => { diff --git a/app/assets/javascripts/ui_models/app_state/files_state.ts b/app/assets/javascripts/ui_models/app_state/files_state.ts new file mode 100644 index 000000000..526b48a33 --- /dev/null +++ b/app/assets/javascripts/ui_models/app_state/files_state.ts @@ -0,0 +1,142 @@ +import { + ClassicFileReader, + StreamingFileReader, + StreamingFileSaver, + ClassicFileSaver, +} from '@standardnotes/filepicker'; +import { SNFile } from '@standardnotes/snjs'; +import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'; + +import { WebApplication } from '../application'; + +export class FilesState { + constructor(private application: WebApplication) {} + + public async downloadFile(file: SNFile): Promise { + let downloadingToastId = ''; + + try { + const saver = StreamingFileSaver.available() + ? new StreamingFileSaver(file.nameWithExt) + : new ClassicFileSaver(); + + const isUsingStreamingSaver = saver instanceof StreamingFileSaver; + + if (isUsingStreamingSaver) { + await saver.selectFileToSaveTo(); + } + + downloadingToastId = addToast({ + type: ToastType.Loading, + message: `Downloading file...`, + }); + + await this.application.files.downloadFile( + file, + async (decryptedBytes: Uint8Array) => { + if (isUsingStreamingSaver) { + await saver.pushBytes(decryptedBytes); + } else { + saver.saveFile(file.nameWithExt, decryptedBytes); + } + } + ); + + if (isUsingStreamingSaver) { + await saver.finish(); + } + + addToast({ + type: ToastType.Success, + message: 'Successfully downloaded file', + }); + } catch (error) { + console.error(error); + + addToast({ + type: ToastType.Error, + message: 'There was an error while downloading the file', + }); + } + + if (downloadingToastId.length > 0) { + dismissToast(downloadingToastId); + } + } + + public async uploadNewFile(fileOrHandle?: File | FileSystemFileHandle) { + let toastId = ''; + + try { + const minimumChunkSize = this.application.files.minimumChunkSize(); + + const picker = StreamingFileReader.available() + ? StreamingFileReader + : ClassicFileReader; + + const selectedFiles = + fileOrHandle instanceof File + ? [fileOrHandle] + : StreamingFileReader.available() && + fileOrHandle instanceof FileSystemFileHandle + ? await StreamingFileReader.getFilesFromHandles([fileOrHandle]) + : await picker.selectFiles(); + + const uploadedFiles: SNFile[] = []; + + for (const file of selectedFiles) { + const operation = await this.application.files.beginNewFileUpload(); + + const onChunk = async ( + chunk: Uint8Array, + index: number, + isLast: boolean + ) => { + await this.application.files.pushBytesForUpload( + operation, + chunk, + index, + isLast + ); + }; + + toastId = addToast({ + type: ToastType.Loading, + message: `Uploading file "${file.name}"...`, + }); + + const fileResult = await picker.readFile( + file, + minimumChunkSize, + onChunk + ); + + const uploadedFile = await this.application.files.finishUpload( + operation, + fileResult.name, + fileResult.ext + ); + + uploadedFiles.push(uploadedFile); + + dismissToast(toastId); + addToast({ + type: ToastType.Success, + message: `Uploaded file "${uploadedFile.nameWithExt}"`, + }); + } + + return uploadedFiles; + } catch (error) { + console.error(error); + + if (toastId.length > 0) { + dismissToast(toastId); + } + addToast({ + type: ToastType.Error, + message: 'There was an error while uploading the file', + }); + } + } +} diff --git a/app/assets/javascripts/utils/index.ts b/app/assets/javascripts/utils/index.ts index c802e760f..f16780ca7 100644 --- a/app/assets/javascripts/utils/index.ts +++ b/app/assets/javascripts/utils/index.ts @@ -1,6 +1,10 @@ import { Platform, platformFromString } from '@standardnotes/snjs'; import { IsDesktopPlatform, IsWebPlatform } from '@/version'; -import { EMAIL_REGEX } from '../constants'; +import { + BYTES_IN_ONE_KILOBYTE, + BYTES_IN_ONE_MEGABYTE, + EMAIL_REGEX, +} from '../constants'; export { isMobile } from './isMobile'; declare const process: { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 503ca21b4..7ec1f9d25 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -258,6 +258,11 @@ margin-right: 3rem; } +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + .my-0\.5 { margin-top: 0.125rem; margin-bottom: 0.125rem; @@ -328,6 +333,10 @@ width: 0.75rem; } +.w-18 { + width: 4.5rem; +} + .w-26 { width: 6.5rem; } @@ -428,6 +437,10 @@ max-height: 1.25rem; } +.max-h-110 { + max-height: 27.5rem; +} + .border-danger { border-color: var(--sn-stylekit-danger-color); } @@ -517,6 +530,11 @@ padding-right: 0; } +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + .px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; @@ -576,6 +594,10 @@ font-weight: 500; } +.sticky { + position: sticky; +} + .top-30\% { top: 30%; } @@ -640,6 +662,10 @@ right: 0; } +.right-2 { + right: 0.5rem; +} + .-right-2 { right: -0.5rem; } @@ -906,6 +932,10 @@ var(--sn-stylekit-info-color) -1px -1px 0px 0px inset; } +.focus\:shadow-bottom:focus { + box-shadow: currentcolor 0px -1px 0px 0px inset, currentcolor 0px 1px 0px 0px; +} + .bg-note-size-warning { background-color: rgba(235, 173, 0, 0.08); } @@ -960,13 +990,15 @@ } .transition { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-timing-function: cubic-bezier(.4,0,.2,1); + transition-property: color, background-color, border-color, + text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, + backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 100ms; } .animate-fade-from-top { - animation: fade-from-top .2s ease-out; + animation: fade-from-top 0.2s ease-out; } @keyframes fade-from-top { diff --git a/package.json b/package.json index 6a64373f4..6d07eb49e 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,14 @@ "@babel/preset-typescript": "^7.16.7", "@reach/disclosure": "^0.16.2", "@reach/visually-hidden": "^0.16.0", - "@standardnotes/responses": "1.3.2", - "@standardnotes/services": "1.5.4", + "@standardnotes/responses": "1.3.4", + "@standardnotes/services": "1.5.6", "@standardnotes/stylekit": "5.15.0", "@svgr/webpack": "^6.2.1", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.179", "@types/react": "^17.0.39", + "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "apply-loader": "^2.0.0", @@ -71,7 +72,7 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@bugsnag/js": "^7.16.1", + "@bugsnag/js": "^7.16.2", "@reach/alert": "^0.16.0", "@reach/alert-dialog": "^0.16.2", "@reach/checkbox": "^0.16.0", @@ -79,10 +80,11 @@ "@reach/listbox": "^0.16.2", "@reach/tooltip": "^0.16.2", "@standardnotes/components": "1.7.10", - "@standardnotes/features": "1.34.4", + "@standardnotes/features": "1.34.5", + "@standardnotes/filepicker": "1.8.0", "@standardnotes/settings": "1.12.0", "@standardnotes/sncrypto-web": "1.7.3", - "@standardnotes/snjs": "2.77.2", + "@standardnotes/snjs": "2.79.0", "mobx": "^6.4.2", "mobx-react-lite": "^3.3.0", "preact": "^10.6.6", diff --git a/yarn.lock b/yarn.lock index 3aa7534f6..9550cec07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,10 +1751,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bugsnag/browser@^7.16.1": - version "7.16.1" - resolved "https://registry.yarnpkg.com/@bugsnag/browser/-/browser-7.16.1.tgz#652aa3ed64e51ba0015878d252a08917429bba03" - integrity sha512-Tq9fWpwmqdOsbedYL67GzsTKrG5MERIKtnKCi5FyvFjTj143b6as0pwj7LWQ+Eh8grWlR7S11+VvJmb8xnY8Tg== +"@bugsnag/browser@^7.16.2": + version "7.16.2" + resolved "https://registry.yarnpkg.com/@bugsnag/browser/-/browser-7.16.2.tgz#b6fc7ebaeae4800d195b660abc770caf33670e03" + integrity sha512-iBbAmjTDe0I6WPTHi3wIcmKu3ykydtT6fc8atJA65rzgDLMlTM1Wnwz4Ny1cn0bVouLGa48BRiOJ27Rwy7QRYA== dependencies: "@bugsnag/core" "^7.16.1" @@ -1774,18 +1774,18 @@ resolved "https://registry.yarnpkg.com/@bugsnag/cuid/-/cuid-3.0.0.tgz#2ee7642a30aee6dc86f5e7f824653741e42e5c35" integrity sha512-LOt8aaBI+KvOQGneBtpuCz3YqzyEAehd1f3nC5yr9TIYW1+IzYKa2xWS4EiMz5pPOnRPHkyyS5t/wmSmN51Gjg== -"@bugsnag/js@^7.16.1": - version "7.16.1" - resolved "https://registry.yarnpkg.com/@bugsnag/js/-/js-7.16.1.tgz#4a4ec2c7f3e047333e7d15eb53cb11f165b7067f" - integrity sha512-yb83OmsbIMDJhX3hHhbHl5StN72feqdr/Ctq7gqsdcfOHNb2121Edf2EbegPJKZhFqSik66vWwiVbGJ6CdS/UQ== +"@bugsnag/js@^7.16.2": + version "7.16.2" + resolved "https://registry.yarnpkg.com/@bugsnag/js/-/js-7.16.2.tgz#fb15ec9cc5980f0b210aecc7b740274e50400a91" + integrity sha512-AzV0PtG3SZt+HnA2JmRJeI60aDNZsIJbEEAZIWZeATvWBt5RdVdsWKllM1SkTvURfxfdAVd4Xry3BgVrh8nEbg== dependencies: - "@bugsnag/browser" "^7.16.1" - "@bugsnag/node" "^7.16.1" + "@bugsnag/browser" "^7.16.2" + "@bugsnag/node" "^7.16.2" -"@bugsnag/node@^7.16.1": - version "7.16.1" - resolved "https://registry.yarnpkg.com/@bugsnag/node/-/node-7.16.1.tgz#473bb6eeb346b418295b49e4c4576e0004af4901" - integrity sha512-9zBA1IfDTbLKMoDltdhELpTd1e+b5+vUW4j40zGA+4SYIe64XNZKShfqRdvij7embvC1iHQ9UpuPRSk60P6Dng== +"@bugsnag/node@^7.16.2": + version "7.16.2" + resolved "https://registry.yarnpkg.com/@bugsnag/node/-/node-7.16.2.tgz#8ac1b41786306d8917fb9fe222ada74fe0c4c6d5" + integrity sha512-V5pND701cIYGzjjTwt0tuvAU1YyPB9h7vo5F/DzrDHRPmCINA/oVbc0Twco87knc2VPe8ntGFqTicTY65iOWzg== dependencies: "@bugsnag/core" "^7.16.1" byline "^5.0.0" @@ -2313,10 +2313,10 @@ dependencies: "@standardnotes/common" "^1.15.3" -"@standardnotes/auth@^3.17.3": - version "3.17.3" - resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.17.3.tgz#a00f10faa0fb2a7dd76509d3b678f85818aad63c" - integrity sha512-tb5ylXuDBPhgeZZynNsMk83N74NMMV9z6M9hyrwuK5HbKWM5r5L9U8lwFawG8flqTKpYzPeWxmaRFZT/5qR22Q== +"@standardnotes/auth@^3.17.4": + version "3.17.4" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.17.4.tgz#ab2449a280ee6ec794fe397c9d8387e105c6c644" + integrity sha512-0710hUiYoRFjABfUFPlyOIyCMx0gC0rlJtFdPYK7WHXf0bfxO0JiSXeWbNSvV0QVGqHIkcUjGmdyE6cJEKTh9g== dependencies: "@standardnotes/common" "^1.15.3" jsonwebtoken "^8.5.1" @@ -2331,50 +2331,55 @@ resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.10.tgz#1b135edda74521c5143760c15df3ec88d6001d5a" integrity sha512-s+rxAw0o3wlAyq+MMjV7Hh31C+CckZJUer/ueWbRpL60YRl4JYZ7Tbx6ciw6VkxXFwYjW+aIOU0FOASjJrvpmg== -"@standardnotes/domain-events@^2.23.26": - version "2.23.26" - resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.23.26.tgz#310d44e8cc3524ddf6945907b0d0a4fa273d84a8" - integrity sha512-+GS6/Nc9yIXLL+Q9xFKynA+pMTik4Q7sL5VvJC99fRjrYeXZrxPBbJcXZdwtY61F58QYD86MQwpzhEuLGGsseg== +"@standardnotes/domain-events@^2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.24.1.tgz#dc578242297d3a6ae7ccdabde97f0229a0e6294c" + integrity sha512-UAOlTdH4WWkpIfi5fAKtLCCj3kb4cecxIhj57tuLORidq0z9VrY+9pFN86yIuLWGJPsaO22B1pLX/mwEDrOJPw== dependencies: - "@standardnotes/auth" "^3.17.3" - "@standardnotes/features" "^1.34.4" + "@standardnotes/auth" "^3.17.4" + "@standardnotes/features" "^1.34.5" -"@standardnotes/features@1.34.4", "@standardnotes/features@^1.34.4": - version "1.34.4" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.34.4.tgz#171ffb481600195c2cb4f8d23e630ce3f840932a" - integrity sha512-Ej+9s2H208dF8M/hXKkQ+MzGzM4qioPvfF0wmZ/ovz/PyIkGAOGU/l3zxJPI4vov4W7Xvxk6jMAxa1LEOerDuQ== +"@standardnotes/features@1.34.5", "@standardnotes/features@^1.34.5": + version "1.34.5" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.34.5.tgz#3077cf8c6c353694b3a8c0350961a0262f3139e8" + integrity sha512-b3T67XkMaiNR4D2n6/wszMK+eCEkX6xdYxqCeJYl8ofeM25AtznLMFcRQtCCOoNso+fld8vwCF+VBVp/l5yuDw== dependencies: - "@standardnotes/auth" "^3.17.3" + "@standardnotes/auth" "^3.17.4" "@standardnotes/common" "^1.15.3" -"@standardnotes/payloads@^1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@standardnotes/payloads/-/payloads-1.4.3.tgz#8d146a61d8bf173835ee54a683d1e6cc95ca15ee" - integrity sha512-mwC2EBHjniZBF3eSfkNE45VLGOn0xKWkOcAFb0sSLjouB4Mk90/CG+9gIGVZX8WcpG62A/vVcgvvJtcOtNfK6w== +"@standardnotes/filepicker@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.8.0.tgz#f8d85350c4b4022235e3017b0b2c7841882eef4f" + integrity sha512-xgFoD+aHFCKV5pAbhKNCyyhUL18G9l2Aep6eiQ5gxB55l8CcNHlLBi5qw5i1we07NdCwIJ3yP3aVKI+7qe22yQ== + +"@standardnotes/payloads@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@standardnotes/payloads/-/payloads-1.4.4.tgz#c6414069a17af12b9cfbfb4f7dfa72303d5cdd5d" + integrity sha512-gJuaSLGZgtCXP9iJJByKgZ41wn5KRP5QvQFwxoOQWIMy1KIBNItMVSHDZn4AVwi9S8qeMj9jdONXzzwX+IYGfQ== dependencies: "@standardnotes/applications" "^1.1.3" "@standardnotes/common" "^1.15.3" - "@standardnotes/features" "^1.34.4" + "@standardnotes/features" "^1.34.5" "@standardnotes/utils" "^1.2.3" -"@standardnotes/responses@1.3.2", "@standardnotes/responses@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.3.2.tgz#fc967bc8013bc1df3e627094cce7b44e99fb8ecc" - integrity sha512-d7U9IpngnnAxmT0S0uKPdQgklzykBrHZNj2UlcbwkhAjbSf1mB4nPaEQFa9qjl30YeasK+0EbJjf2G8ThCcE8Q== +"@standardnotes/responses@1.3.4", "@standardnotes/responses@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.3.4.tgz#94dae8bafb481dd9b263f78af7558a2e762cf0ed" + integrity sha512-4oxPppADhKJ2K1X4SGRiUuFfzbYIrK57sNP1V8HJod2ULp4IPPZbkvpSmtVaSUeyPGqbpszSltQBVCblgfe3nQ== dependencies: - "@standardnotes/auth" "^3.17.3" + "@standardnotes/auth" "^3.17.4" "@standardnotes/common" "^1.15.3" - "@standardnotes/features" "^1.34.4" - "@standardnotes/payloads" "^1.4.3" + "@standardnotes/features" "^1.34.5" + "@standardnotes/payloads" "^1.4.4" -"@standardnotes/services@1.5.4", "@standardnotes/services@^1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.5.4.tgz#16067c9bd0d8a54e5ea2295e97cbbe4327e80811" - integrity sha512-qyq2KzvDWqUvBLXAdW8KQI1AqpdynSItn3iBzDM+h4U4rJMaAmkPoWtMxVcBAjz7cdHC5xku6t/iAvvKFKqUkA== +"@standardnotes/services@1.5.6", "@standardnotes/services@^1.5.6": + version "1.5.6" + resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.5.6.tgz#53938d82a8492d88097dfcfb714ca472c8557ab5" + integrity sha512-2G7lGC+aYO/t0viIhL5EZKneJM+wqRfZNPw83SsjW1YR4hIAWz7C6sUwAnkWorCeiSWQoXRI2O0AhDaCHpVUXg== dependencies: "@standardnotes/applications" "^1.1.3" "@standardnotes/common" "^1.15.3" - "@standardnotes/responses" "^1.3.2" + "@standardnotes/responses" "^1.3.4" "@standardnotes/utils" "^1.2.3" "@standardnotes/settings@1.12.0", "@standardnotes/settings@^1.12.0": @@ -2396,19 +2401,19 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.77.2": - version "2.77.2" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.77.2.tgz#f9a687a81f84b1978b1edf5e0527f4dc2702bef8" - integrity sha512-r5bdOVltdhgJgTI9CrlMoC/jCmvEeLIgkMy5RtT5K6EBOnxu9soxsBA2cvPo6nIs8+m6BS4psLpMBy93Kx/D5w== +"@standardnotes/snjs@2.79.0": + version "2.79.0" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.79.0.tgz#645f13c068972ce8c90132312e74eeebf9a375bb" + integrity sha512-fDLuQaAzZrBMjRhhkUy7B8xAzx47prFiLtEPsAJkZNHumkoB1KGU2iEE5ZxnlHc008ilLX0Nj4r5tqGXKxUFQA== dependencies: "@standardnotes/applications" "^1.1.3" - "@standardnotes/auth" "^3.17.3" + "@standardnotes/auth" "^3.17.4" "@standardnotes/common" "^1.15.3" - "@standardnotes/domain-events" "^2.23.26" - "@standardnotes/features" "^1.34.4" - "@standardnotes/payloads" "^1.4.3" - "@standardnotes/responses" "^1.3.2" - "@standardnotes/services" "^1.5.4" + "@standardnotes/domain-events" "^2.24.1" + "@standardnotes/features" "^1.34.5" + "@standardnotes/payloads" "^1.4.4" + "@standardnotes/responses" "^1.3.4" + "@standardnotes/services" "^1.5.6" "@standardnotes/settings" "^1.12.0" "@standardnotes/sncrypto-common" "^1.7.3" "@standardnotes/utils" "^1.2.3" @@ -2814,6 +2819,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/wicg-file-system-access@^2020.9.5": + version "2020.9.5" + resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz#4a0c8f3d1ed101525f329e86c978f7735404474f" + integrity sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA== + "@types/ws@^8.2.2": version "8.2.2" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21"