feat: add files popover in note toolbar (#913)

This commit is contained in:
Aman Harwara
2022-03-10 13:51:28 +05:30
committed by GitHub
parent 87631dcb0d
commit b31afee108
18 changed files with 1269 additions and 105 deletions

View File

@@ -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<Props, State> {
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<Props, State> {
}
}
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') &&

View File

@@ -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<void>;
};
export const AttachedFilesButton: FunctionComponent<Props> = 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<number | 'auto'>('auto');
const buttonRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(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 (
<div ref={containerRef}>
<Disclosure open={open} onChange={toggleAttachedFilesMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false);
}
}}
ref={buttonRef}
className={`sn-icon-button border-contrast ${
attachedFilesCount > 0 ? 'py-1 px-3' : ''
}`}
>
<VisuallyHidden>Attached files</VisuallyHidden>
<Icon type="attachment-file" className="block" />
{attachedFilesCount > 0 && (
<span className="ml-2">{attachedFilesCount}</span>
)}
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
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 && (
<PopoverDragNDropWrapper
application={application}
appState={appState}
note={note}
fileActionHandler={handleFileAction}
/>
)}
</DisclosurePanel>
</Disclosure>
</div>
);
}
);

View File

@@ -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<PopoverTabs>;
};
export const AttachedFilesPopover: FunctionComponent<Props> = observer(
({
application,
appState,
note,
fileActionHandler,
currentTab,
setCurrentTab,
}) => {
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
const [allFiles, setAllFiles] = useState<SNFile[]>([]);
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 (
<div className="flex flex-col">
<div className="flex border-0 border-b-1 border-solid border-main">
<button
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
currentTab === PopoverTabs.AttachedFiles
? 'color-info font-medium shadow-bottom'
: 'color-text'
}`}
onClick={() => {
setCurrentTab(PopoverTabs.AttachedFiles);
}}
>
Attached
</button>
<button
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
currentTab === PopoverTabs.AllFiles
? 'color-info font-medium shadow-bottom'
: 'color-text'
}`}
onClick={() => {
setCurrentTab(PopoverTabs.AllFiles);
}}
>
All files
</button>
</div>
<div className="min-h-0 max-h-110 overflow-y-auto">
{filteredList.length > 0 || searchQuery.length > 0 ? (
<div className="sticky top-0 left-0 p-3 bg-default border-0 border-b-1 border-solid border-main">
<div className="relative">
<input
type="text"
className="w-full rounded py-1.5 px-3 text-input bg-default border-solid border-1 border-main"
placeholder="Search files..."
value={searchQuery}
onInput={(e) => {
setSearchQuery((e.target as HTMLInputElement).value);
}}
/>
{searchQuery.length > 0 && (
<button
className="flex absolute right-2 p-0 bg-transparent border-0 top-1/2 -translate-y-1/2 cursor-pointer"
onClick={() => {
setSearchQuery('');
}}
>
<Icon
type="clear-circle-filled"
className="color-neutral"
/>
</button>
)}
</div>
</div>
) : null}
{filteredList.length > 0 ? (
filteredList.map((file: SNFile) => {
return (
<PopoverFileItem
key={file.uuid}
file={file}
isAttachedToNote={attachedFiles.includes(file)}
handleFileAction={fileActionHandler}
/>
);
})
) : (
<div className="flex flex-col items-center justify-center w-full py-8">
<div className="w-18 h-18 mb-2">
<FilesIllustration
style={{
transform: 'scale(0.6)',
transformOrigin: 'top left',
}}
/>
</div>
<div className="text-sm font-medium mb-3">
{searchQuery.length > 0
? 'No result found'
: currentTab === PopoverTabs.AttachedFiles
? 'No files attached to this note'
: 'No files found in this account'}
</div>
<Button type="normal" onClick={handleAttachFilesClick}>
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'}{' '}
files
</Button>
<div className="text-xs color-grey-0 mt-2">
Or drop your files here
</div>
</div>
)}
</div>
{filteredList.length > 0 && (
<button
className="sn-dropdown-item py-3 border-0 border-t-1px border-solid border-main focus:bg-info-backdrop"
onClick={handleAttachFilesClick}
>
<Icon type="add" className="mr-2 color-neutral" />
{currentTab === PopoverTabs.AttachedFiles
? 'Attach'
: 'Upload'}{' '}
files
</button>
)}
</div>
);
}
);

View File

@@ -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<boolean>;
};
export const PopoverDragNDropWrapper: FunctionComponent<
PopoverWrapperProps
> = ({ fileActionHandler, appState, application, note }) => {
const dropzoneRef = useRef<HTMLDivElement>(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 (
<div
ref={dropzoneRef}
className="focus:shadow-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
style={{
border: isDragging ? '2px dashed var(--sn-stylekit-info-color)' : '',
}}
>
<AttachedFilesPopover
application={application}
appState={appState}
note={note}
fileActionHandler={fileActionHandler}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
</div>
);
};

View File

@@ -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 <IconComponent className="flex-shrink-0" />;
};
export type PopoverFileItemProps = {
file: SNFile;
isAttachedToNote: boolean;
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
};
export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
file,
isAttachedToNote,
handleFileAction,
}) => {
const [fileName, setFileName] = useState(file.nameWithExt);
const [isRenamingFile, setIsRenamingFile] = useState(false);
const fileNameInputRef = useRef<HTMLInputElement>(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 (
<div className="flex items-center justify-between p-3">
<div className="flex items-center">
{getIconForFileType(file.ext ?? '')}
<div className="flex flex-col mx-4">
{isRenamingFile ? (
<input
type="text"
className="text-input px-1.5 py-1 mb-1 border-1 border-solid border-main bg-transparent color-foreground"
value={fileName}
ref={fileNameInputRef}
onInput={handleFileNameInput}
onKeyDown={handleFileNameInputKeyDown}
/>
) : (
<div className="text-sm mb-1">{file.nameWithExt}</div>
)}
<div className="text-xs color-grey-0">
{file.created_at.toLocaleString()} ·{' '}
{formatSizeToReadableString(file.size)}
</div>
</div>
</div>
<PopoverFileSubmenu
file={file}
isAttachedToNote={isAttachedToNote}
handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile}
/>
</div>
);
};

View File

@@ -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;
};
};

View File

@@ -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<PopoverFileItemProps, 'renameFile'> & {
setIsRenamingFile: StateUpdater<boolean>;
};
export const PopoverFileSubmenu: FunctionComponent<Props> = ({
file,
isAttachedToNote,
handleFileAction,
setIsRenamingFile,
}) => {
const menuContainerRef = useRef<HTMLDivElement>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isFileProtected, setIsFileProtected] = useState(file.protected);
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
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 (
<div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleMenu}>
<DisclosureButton
ref={menuButtonRef}
onBlur={closeOnBlur}
className="w-7 h-7 p-1 rounded-full border-0 bg-transparent hover:bg-contrast cursor-pointer"
>
<Icon type="more" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
style={{
...menuStyle,
position: 'fixed',
}}
className="sn-dropdown flex flex-col max-h-120 min-w-60 py-1 fixed overflow-y-auto"
>
{isMenuOpen && (
<>
{isAttachedToNote ? (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DetachFileToNote,
payload: file,
});
closeMenu();
}}
>
<Icon type="link-off" className="mr-2 color-neutral" />
Detach from note
</button>
) : (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.AttachFileToNote,
payload: file,
});
closeMenu();
}}
>
<Icon type="link" className="mr-2 color-neutral" />
Attach to note
</button>
)}
<div className="min-h-1px my-1 bg-border"></div>
<button
className="sn-dropdown-item justify-between focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.ToggleFileProtection,
payload: file,
callback: (isProtected: boolean) => {
setIsFileProtected(isProtected);
},
});
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className="mr-2 color-neutral" />
Password protection
</span>
<Switch
className="px-0 pointer-events-none"
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
checked={isFileProtected}
/>
</button>
<div className="min-h-1px my-1 bg-border"></div>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
handleFileAction({
type: PopoverFileItemActionType.DownloadFile,
payload: file,
});
closeMenu();
}}
>
<Icon type="download" className="mr-2 color-neutral" />
Download
</button>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item focus:bg-info-backdrop"
onClick={() => {
setIsRenamingFile(true);
}}
>
<Icon type="pencil" className="mr-2 color-neutral" />
Rename
</button>
</>
)}
</DisclosurePanel>
</Disclosure>
</div>
);
};

View File

@@ -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,

View File

@@ -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<Props, State> {
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<Props, State> {
/** @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<Props, State> {
)}
</div>
</div>
{this.state.isEntitledToFiles &&
window.enabledUnfinishedFeatures && (
<div className="mr-3">
<AttachedFilesButton
application={this.application}
appState={this.appState}
onClickPreprocessing={
this.ensureNoteIsInsertedBeforeUIAction
}
/>
</div>
)}
<div className="mr-3">
<ChangeEditorButton
application={this.application}

View File

@@ -38,7 +38,6 @@ export const AddTagOption: FunctionComponent<Props> = observer(
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
if (menuPosition) {
setMenuStyle(menuPosition);
console.log(menuPosition);
}
}
@@ -53,7 +52,6 @@ export const AddTagOption: FunctionComponent<Props> = observer(
if (newMenuPosition) {
setMenuStyle(newMenuPosition);
console.log(newMenuPosition);
}
}, []);

View File

@@ -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();
}
},

View File

@@ -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;

View File

@@ -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 = () => {

View File

@@ -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<void> {
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',
});
}
}
}

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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",

122
yarn.lock
View File

@@ -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"