feat: add files popover in note toolbar (#913)
This commit is contained in:
@@ -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') &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
142
app/assets/javascripts/ui_models/app_state/files_state.ts
Normal file
142
app/assets/javascripts/ui_models/app_state/files_state.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
12
package.json
12
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",
|
||||
|
||||
122
yarn.lock
122
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"
|
||||
|
||||
Reference in New Issue
Block a user