feat: add files popover in note toolbar (#913)
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
|||||||
removeFromArray,
|
removeFromArray,
|
||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/constants';
|
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/constants';
|
||||||
import { STRING_DEFAULT_FILE_ERROR } from '@/strings';
|
|
||||||
import { alertDialog } from '@/services/alertService';
|
import { alertDialog } from '@/services/alertService';
|
||||||
import { WebAppEvent, WebApplication } from '@/ui_models/application';
|
import { WebAppEvent, WebApplication } from '@/ui_models/application';
|
||||||
import { PureComponent } from '@/components/Abstract/PureComponent';
|
import { PureComponent } from '@/components/Abstract/PureComponent';
|
||||||
@@ -51,17 +50,10 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
appClass: '',
|
appClass: '',
|
||||||
challenges: [],
|
challenges: [],
|
||||||
};
|
};
|
||||||
this.onDragDrop = this.onDragDrop.bind(this);
|
|
||||||
this.onDragOver = this.onDragOver.bind(this);
|
|
||||||
this.addDragDropHandlers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
(this.application as unknown) = undefined;
|
(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();
|
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() {
|
async handleDemoSignInFromParams() {
|
||||||
if (
|
if (
|
||||||
window.location.href.includes('demo') &&
|
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,
|
ArrowLeftIcon,
|
||||||
ArrowsSortDownIcon,
|
ArrowsSortDownIcon,
|
||||||
ArrowsSortUpIcon,
|
ArrowsSortUpIcon,
|
||||||
|
AttachmentFileIcon,
|
||||||
AuthenticatorIcon,
|
AuthenticatorIcon,
|
||||||
CheckBoldIcon,
|
CheckBoldIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
CloudOffIcon,
|
CloudOffIcon,
|
||||||
|
ClearCircleFilledIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
@@ -25,12 +27,22 @@ import {
|
|||||||
EmailIcon,
|
EmailIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
|
FileDocIcon,
|
||||||
|
FileImageIcon,
|
||||||
|
FileMovIcon,
|
||||||
|
FileMusicIcon,
|
||||||
|
FileOtherIcon,
|
||||||
|
FilePdfIcon,
|
||||||
|
FilePptIcon,
|
||||||
|
FileXlsIcon,
|
||||||
|
FileZipIcon,
|
||||||
HashtagIcon,
|
HashtagIcon,
|
||||||
HashtagOffIcon,
|
HashtagOffIcon,
|
||||||
HelpIcon,
|
HelpIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
KeyboardIcon,
|
KeyboardIcon,
|
||||||
|
LinkIcon,
|
||||||
LinkOffIcon,
|
LinkOffIcon,
|
||||||
ListBulleted,
|
ListBulleted,
|
||||||
ListedIcon,
|
ListedIcon,
|
||||||
@@ -44,9 +56,9 @@ import {
|
|||||||
MoreIcon,
|
MoreIcon,
|
||||||
NotesIcon,
|
NotesIcon,
|
||||||
PasswordIcon,
|
PasswordIcon,
|
||||||
PencilOffIcon,
|
|
||||||
PencilFilledIcon,
|
PencilFilledIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
|
PencilOffIcon,
|
||||||
PinFilledIcon,
|
PinFilledIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
PlainTextIcon,
|
PlainTextIcon,
|
||||||
@@ -75,17 +87,28 @@ import {
|
|||||||
WindowIcon,
|
WindowIcon,
|
||||||
} from '@standardnotes/stylekit';
|
} from '@standardnotes/stylekit';
|
||||||
|
|
||||||
const ICONS = {
|
export const ICONS = {
|
||||||
'account-circle': AccountCircleIcon,
|
'account-circle': AccountCircleIcon,
|
||||||
'arrow-left': ArrowLeftIcon,
|
'arrow-left': ArrowLeftIcon,
|
||||||
'arrows-sort-down': ArrowsSortDownIcon,
|
'arrows-sort-down': ArrowsSortDownIcon,
|
||||||
'arrows-sort-up': ArrowsSortUpIcon,
|
'arrows-sort-up': ArrowsSortUpIcon,
|
||||||
|
'attachment-file': AttachmentFileIcon,
|
||||||
'check-bold': CheckBoldIcon,
|
'check-bold': CheckBoldIcon,
|
||||||
'check-circle': CheckCircleIcon,
|
'check-circle': CheckCircleIcon,
|
||||||
'chevron-down': ChevronDownIcon,
|
'chevron-down': ChevronDownIcon,
|
||||||
'chevron-right': ChevronRightIcon,
|
'chevron-right': ChevronRightIcon,
|
||||||
'cloud-off': CloudOffIcon,
|
'cloud-off': CloudOffIcon,
|
||||||
|
'clear-circle-filled': ClearCircleFilledIcon,
|
||||||
'eye-off': EyeOffIcon,
|
'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,
|
'hashtag-off': HashtagOffIcon,
|
||||||
'link-off': LinkOffIcon,
|
'link-off': LinkOffIcon,
|
||||||
'list-bulleted': ListBulleted,
|
'list-bulleted': ListBulleted,
|
||||||
@@ -121,6 +144,7 @@ const ICONS = {
|
|||||||
history: HistoryIcon,
|
history: HistoryIcon,
|
||||||
info: InfoIcon,
|
info: InfoIcon,
|
||||||
keyboard: KeyboardIcon,
|
keyboard: KeyboardIcon,
|
||||||
|
link: LinkIcon,
|
||||||
listed: ListedIcon,
|
listed: ListedIcon,
|
||||||
lock: LockIcon,
|
lock: LockIcon,
|
||||||
markdown: MarkdownIcon,
|
markdown: MarkdownIcon,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
ItemMutator,
|
ItemMutator,
|
||||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||||
NoteViewController,
|
NoteViewController,
|
||||||
|
FeatureIdentifier,
|
||||||
|
FeatureStatus,
|
||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { debounce, isDesktopApplication } from '@/utils';
|
import { debounce, isDesktopApplication } from '@/utils';
|
||||||
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
||||||
@@ -37,6 +39,7 @@ import { ComponentView } from '../ComponentView';
|
|||||||
import { PanelSide, PanelResizer, PanelResizeType } from '../PanelResizer';
|
import { PanelSide, PanelResizer, PanelResizeType } from '../PanelResizer';
|
||||||
import { ElementIds } from '@/element_ids';
|
import { ElementIds } from '@/element_ids';
|
||||||
import { ChangeEditorButton } from '../ChangeEditorButton';
|
import { ChangeEditorButton } from '../ChangeEditorButton';
|
||||||
|
import { AttachedFilesButton } from '../AttachedFilesPopover/AttachedFilesButton';
|
||||||
|
|
||||||
const MINIMUM_STATUS_DURATION = 400;
|
const MINIMUM_STATUS_DURATION = 400;
|
||||||
const TEXTAREA_DEBOUNCE = 100;
|
const TEXTAREA_DEBOUNCE = 100;
|
||||||
@@ -100,6 +103,7 @@ type State = {
|
|||||||
editorTitle: string;
|
editorTitle: string;
|
||||||
editorText: string;
|
editorText: string;
|
||||||
isDesktop?: boolean;
|
isDesktop?: boolean;
|
||||||
|
isEntitledToFiles: boolean;
|
||||||
lockText: string;
|
lockText: string;
|
||||||
marginResizersEnabled?: boolean;
|
marginResizersEnabled?: boolean;
|
||||||
monospaceFont?: boolean;
|
monospaceFont?: boolean;
|
||||||
@@ -168,6 +172,9 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
editorText: '',
|
editorText: '',
|
||||||
editorTitle: '',
|
editorTitle: '',
|
||||||
isDesktop: isDesktopApplication(),
|
isDesktop: isDesktopApplication(),
|
||||||
|
isEntitledToFiles:
|
||||||
|
this.application.features.getFeatureStatus(FeatureIdentifier.Files) ===
|
||||||
|
FeatureStatus.Entitled,
|
||||||
lockText: 'Note Editing Disabled',
|
lockText: 'Note Editing Disabled',
|
||||||
noteStatus: undefined,
|
noteStatus: undefined,
|
||||||
noteLocked: this.controller.note.locked,
|
noteLocked: this.controller.note.locked,
|
||||||
@@ -321,6 +328,15 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
/** @override */
|
/** @override */
|
||||||
async onAppEvent(eventName: ApplicationEvent) {
|
async onAppEvent(eventName: ApplicationEvent) {
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
|
case ApplicationEvent.FeaturesUpdated:
|
||||||
|
case ApplicationEvent.UserRolesChanged:
|
||||||
|
this.setState({
|
||||||
|
isEntitledToFiles:
|
||||||
|
this.application.features.getFeatureStatus(
|
||||||
|
FeatureIdentifier.Files
|
||||||
|
) === FeatureStatus.Entitled,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case ApplicationEvent.PreferencesChanged:
|
case ApplicationEvent.PreferencesChanged:
|
||||||
this.reloadPreferences();
|
this.reloadPreferences();
|
||||||
break;
|
break;
|
||||||
@@ -1027,6 +1043,18 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="mr-3">
|
||||||
<ChangeEditorButton
|
<ChangeEditorButton
|
||||||
application={this.application}
|
application={this.application}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export const AddTagOption: FunctionComponent<Props> = observer(
|
|||||||
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
||||||
if (menuPosition) {
|
if (menuPosition) {
|
||||||
setMenuStyle(menuPosition);
|
setMenuStyle(menuPosition);
|
||||||
console.log(menuPosition);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ export const AddTagOption: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
if (newMenuPosition) {
|
if (newMenuPosition) {
|
||||||
setMenuStyle(newMenuPosition);
|
setMenuStyle(newMenuPosition);
|
||||||
console.log(newMenuPosition);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,13 @@ export function useCloseOnClickOutside(
|
|||||||
if (!container.current) {
|
if (!container.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isDescendant = container.current.contains(event.target as Node);
|
const isDescendantOfContainer = container.current.contains(
|
||||||
if (!isDescendant) {
|
event.target as Node
|
||||||
|
);
|
||||||
|
const isDescendantOfDialog = (event.target as HTMLElement).closest(
|
||||||
|
'[role="dialog"]'
|
||||||
|
);
|
||||||
|
if (!isDescendantOfContainer && !isDescendantOfDialog) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ export const NOTES_LIST_SCROLL_THRESHOLD = 200;
|
|||||||
export const MILLISECONDS_IN_A_DAY = 1000 * 60 * 60 * 24;
|
export const MILLISECONDS_IN_A_DAY = 1000 * 60 * 60 * 24;
|
||||||
export const DAYS_IN_A_WEEK = 7;
|
export const DAYS_IN_A_WEEK = 7;
|
||||||
export const DAYS_IN_A_YEAR = 365;
|
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';
|
} from 'mobx';
|
||||||
import { ActionsMenuState } from './actions_menu_state';
|
import { ActionsMenuState } from './actions_menu_state';
|
||||||
import { FeaturesState } from './features_state';
|
import { FeaturesState } from './features_state';
|
||||||
|
import { FilesState } from './files_state';
|
||||||
import { NotesState } from './notes_state';
|
import { NotesState } from './notes_state';
|
||||||
import { NotesViewState } from './notes_view_state';
|
import { NotesViewState } from './notes_view_state';
|
||||||
import { NoteTagsState } from './note_tags_state';
|
import { NoteTagsState } from './note_tags_state';
|
||||||
@@ -89,6 +90,7 @@ export class AppState {
|
|||||||
readonly tags: TagsState;
|
readonly tags: TagsState;
|
||||||
readonly notesView: NotesViewState;
|
readonly notesView: NotesViewState;
|
||||||
readonly subscription: SubscriptionState;
|
readonly subscription: SubscriptionState;
|
||||||
|
readonly files: FilesState;
|
||||||
|
|
||||||
isSessionsModalVisible = false;
|
isSessionsModalVisible = false;
|
||||||
|
|
||||||
@@ -139,6 +141,7 @@ export class AppState {
|
|||||||
this,
|
this,
|
||||||
this.appEventObserverRemovers
|
this.appEventObserverRemovers
|
||||||
);
|
);
|
||||||
|
this.files = new FilesState(application);
|
||||||
this.addAppEventObserver();
|
this.addAppEventObserver();
|
||||||
this.streamNotesAndTags();
|
this.streamNotesAndTags();
|
||||||
this.onVisibilityChange = () => {
|
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 { Platform, platformFromString } from '@standardnotes/snjs';
|
||||||
import { IsDesktopPlatform, IsWebPlatform } from '@/version';
|
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';
|
export { isMobile } from './isMobile';
|
||||||
|
|
||||||
declare const process: {
|
declare const process: {
|
||||||
|
|||||||
@@ -258,6 +258,11 @@
|
|||||||
margin-right: 3rem;
|
margin-right: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx-4 {
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.my-0\.5 {
|
.my-0\.5 {
|
||||||
margin-top: 0.125rem;
|
margin-top: 0.125rem;
|
||||||
margin-bottom: 0.125rem;
|
margin-bottom: 0.125rem;
|
||||||
@@ -328,6 +333,10 @@
|
|||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-18 {
|
||||||
|
width: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-26 {
|
.w-26 {
|
||||||
width: 6.5rem;
|
width: 6.5rem;
|
||||||
}
|
}
|
||||||
@@ -428,6 +437,10 @@
|
|||||||
max-height: 1.25rem;
|
max-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-h-110 {
|
||||||
|
max-height: 27.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.border-danger {
|
.border-danger {
|
||||||
border-color: var(--sn-stylekit-danger-color);
|
border-color: var(--sn-stylekit-danger-color);
|
||||||
}
|
}
|
||||||
@@ -517,6 +530,11 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.px-1\.5 {
|
||||||
|
padding-left: 0.375rem;
|
||||||
|
padding-right: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.px-2\.5 {
|
.px-2\.5 {
|
||||||
padding-left: 0.625rem;
|
padding-left: 0.625rem;
|
||||||
padding-right: 0.625rem;
|
padding-right: 0.625rem;
|
||||||
@@ -576,6 +594,10 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
|
||||||
.top-30\% {
|
.top-30\% {
|
||||||
top: 30%;
|
top: 30%;
|
||||||
}
|
}
|
||||||
@@ -640,6 +662,10 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-2 {
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.-right-2 {
|
.-right-2 {
|
||||||
right: -0.5rem;
|
right: -0.5rem;
|
||||||
}
|
}
|
||||||
@@ -906,6 +932,10 @@
|
|||||||
var(--sn-stylekit-info-color) -1px -1px 0px 0px inset;
|
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 {
|
.bg-note-size-warning {
|
||||||
background-color: rgba(235, 173, 0, 0.08);
|
background-color: rgba(235, 173, 0, 0.08);
|
||||||
}
|
}
|
||||||
@@ -960,13 +990,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.transition {
|
.transition {
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
transition-property: color, background-color, border-color,
|
||||||
transition-timing-function: cubic-bezier(.4,0,.2,1);
|
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;
|
transition-duration: 100ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-from-top {
|
.animate-fade-from-top {
|
||||||
animation: fade-from-top .2s ease-out;
|
animation: fade-from-top 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-from-top {
|
@keyframes fade-from-top {
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -27,13 +27,14 @@
|
|||||||
"@babel/preset-typescript": "^7.16.7",
|
"@babel/preset-typescript": "^7.16.7",
|
||||||
"@reach/disclosure": "^0.16.2",
|
"@reach/disclosure": "^0.16.2",
|
||||||
"@reach/visually-hidden": "^0.16.0",
|
"@reach/visually-hidden": "^0.16.0",
|
||||||
"@standardnotes/responses": "1.3.2",
|
"@standardnotes/responses": "1.3.4",
|
||||||
"@standardnotes/services": "1.5.4",
|
"@standardnotes/services": "1.5.6",
|
||||||
"@standardnotes/stylekit": "5.15.0",
|
"@standardnotes/stylekit": "5.15.0",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@svgr/webpack": "^6.2.1",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/lodash": "^4.14.179",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/wicg-file-system-access": "^2020.9.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@typescript-eslint/parser": "^5.14.0",
|
||||||
"apply-loader": "^2.0.0",
|
"apply-loader": "^2.0.0",
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
"webpack-merge": "^5.8.0"
|
"webpack-merge": "^5.8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bugsnag/js": "^7.16.1",
|
"@bugsnag/js": "^7.16.2",
|
||||||
"@reach/alert": "^0.16.0",
|
"@reach/alert": "^0.16.0",
|
||||||
"@reach/alert-dialog": "^0.16.2",
|
"@reach/alert-dialog": "^0.16.2",
|
||||||
"@reach/checkbox": "^0.16.0",
|
"@reach/checkbox": "^0.16.0",
|
||||||
@@ -79,10 +80,11 @@
|
|||||||
"@reach/listbox": "^0.16.2",
|
"@reach/listbox": "^0.16.2",
|
||||||
"@reach/tooltip": "^0.16.2",
|
"@reach/tooltip": "^0.16.2",
|
||||||
"@standardnotes/components": "1.7.10",
|
"@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/settings": "1.12.0",
|
||||||
"@standardnotes/sncrypto-web": "1.7.3",
|
"@standardnotes/sncrypto-web": "1.7.3",
|
||||||
"@standardnotes/snjs": "2.77.2",
|
"@standardnotes/snjs": "2.79.0",
|
||||||
"mobx": "^6.4.2",
|
"mobx": "^6.4.2",
|
||||||
"mobx-react-lite": "^3.3.0",
|
"mobx-react-lite": "^3.3.0",
|
||||||
"preact": "^10.6.6",
|
"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"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||||
|
|
||||||
"@bugsnag/browser@^7.16.1":
|
"@bugsnag/browser@^7.16.2":
|
||||||
version "7.16.1"
|
version "7.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@bugsnag/browser/-/browser-7.16.1.tgz#652aa3ed64e51ba0015878d252a08917429bba03"
|
resolved "https://registry.yarnpkg.com/@bugsnag/browser/-/browser-7.16.2.tgz#b6fc7ebaeae4800d195b660abc770caf33670e03"
|
||||||
integrity sha512-Tq9fWpwmqdOsbedYL67GzsTKrG5MERIKtnKCi5FyvFjTj143b6as0pwj7LWQ+Eh8grWlR7S11+VvJmb8xnY8Tg==
|
integrity sha512-iBbAmjTDe0I6WPTHi3wIcmKu3ykydtT6fc8atJA65rzgDLMlTM1Wnwz4Ny1cn0bVouLGa48BRiOJ27Rwy7QRYA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bugsnag/core" "^7.16.1"
|
"@bugsnag/core" "^7.16.1"
|
||||||
|
|
||||||
@@ -1774,18 +1774,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@bugsnag/cuid/-/cuid-3.0.0.tgz#2ee7642a30aee6dc86f5e7f824653741e42e5c35"
|
resolved "https://registry.yarnpkg.com/@bugsnag/cuid/-/cuid-3.0.0.tgz#2ee7642a30aee6dc86f5e7f824653741e42e5c35"
|
||||||
integrity sha512-LOt8aaBI+KvOQGneBtpuCz3YqzyEAehd1f3nC5yr9TIYW1+IzYKa2xWS4EiMz5pPOnRPHkyyS5t/wmSmN51Gjg==
|
integrity sha512-LOt8aaBI+KvOQGneBtpuCz3YqzyEAehd1f3nC5yr9TIYW1+IzYKa2xWS4EiMz5pPOnRPHkyyS5t/wmSmN51Gjg==
|
||||||
|
|
||||||
"@bugsnag/js@^7.16.1":
|
"@bugsnag/js@^7.16.2":
|
||||||
version "7.16.1"
|
version "7.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@bugsnag/js/-/js-7.16.1.tgz#4a4ec2c7f3e047333e7d15eb53cb11f165b7067f"
|
resolved "https://registry.yarnpkg.com/@bugsnag/js/-/js-7.16.2.tgz#fb15ec9cc5980f0b210aecc7b740274e50400a91"
|
||||||
integrity sha512-yb83OmsbIMDJhX3hHhbHl5StN72feqdr/Ctq7gqsdcfOHNb2121Edf2EbegPJKZhFqSik66vWwiVbGJ6CdS/UQ==
|
integrity sha512-AzV0PtG3SZt+HnA2JmRJeI60aDNZsIJbEEAZIWZeATvWBt5RdVdsWKllM1SkTvURfxfdAVd4Xry3BgVrh8nEbg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bugsnag/browser" "^7.16.1"
|
"@bugsnag/browser" "^7.16.2"
|
||||||
"@bugsnag/node" "^7.16.1"
|
"@bugsnag/node" "^7.16.2"
|
||||||
|
|
||||||
"@bugsnag/node@^7.16.1":
|
"@bugsnag/node@^7.16.2":
|
||||||
version "7.16.1"
|
version "7.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@bugsnag/node/-/node-7.16.1.tgz#473bb6eeb346b418295b49e4c4576e0004af4901"
|
resolved "https://registry.yarnpkg.com/@bugsnag/node/-/node-7.16.2.tgz#8ac1b41786306d8917fb9fe222ada74fe0c4c6d5"
|
||||||
integrity sha512-9zBA1IfDTbLKMoDltdhELpTd1e+b5+vUW4j40zGA+4SYIe64XNZKShfqRdvij7embvC1iHQ9UpuPRSk60P6Dng==
|
integrity sha512-V5pND701cIYGzjjTwt0tuvAU1YyPB9h7vo5F/DzrDHRPmCINA/oVbc0Twco87knc2VPe8ntGFqTicTY65iOWzg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bugsnag/core" "^7.16.1"
|
"@bugsnag/core" "^7.16.1"
|
||||||
byline "^5.0.0"
|
byline "^5.0.0"
|
||||||
@@ -2313,10 +2313,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
|
|
||||||
"@standardnotes/auth@^3.17.3":
|
"@standardnotes/auth@^3.17.4":
|
||||||
version "3.17.3"
|
version "3.17.4"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.17.3.tgz#a00f10faa0fb2a7dd76509d3b678f85818aad63c"
|
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.17.4.tgz#ab2449a280ee6ec794fe397c9d8387e105c6c644"
|
||||||
integrity sha512-tb5ylXuDBPhgeZZynNsMk83N74NMMV9z6M9hyrwuK5HbKWM5r5L9U8lwFawG8flqTKpYzPeWxmaRFZT/5qR22Q==
|
integrity sha512-0710hUiYoRFjABfUFPlyOIyCMx0gC0rlJtFdPYK7WHXf0bfxO0JiSXeWbNSvV0QVGqHIkcUjGmdyE6cJEKTh9g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
jsonwebtoken "^8.5.1"
|
jsonwebtoken "^8.5.1"
|
||||||
@@ -2331,50 +2331,55 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.10.tgz#1b135edda74521c5143760c15df3ec88d6001d5a"
|
resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.10.tgz#1b135edda74521c5143760c15df3ec88d6001d5a"
|
||||||
integrity sha512-s+rxAw0o3wlAyq+MMjV7Hh31C+CckZJUer/ueWbRpL60YRl4JYZ7Tbx6ciw6VkxXFwYjW+aIOU0FOASjJrvpmg==
|
integrity sha512-s+rxAw0o3wlAyq+MMjV7Hh31C+CckZJUer/ueWbRpL60YRl4JYZ7Tbx6ciw6VkxXFwYjW+aIOU0FOASjJrvpmg==
|
||||||
|
|
||||||
"@standardnotes/domain-events@^2.23.26":
|
"@standardnotes/domain-events@^2.24.1":
|
||||||
version "2.23.26"
|
version "2.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.23.26.tgz#310d44e8cc3524ddf6945907b0d0a4fa273d84a8"
|
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.24.1.tgz#dc578242297d3a6ae7ccdabde97f0229a0e6294c"
|
||||||
integrity sha512-+GS6/Nc9yIXLL+Q9xFKynA+pMTik4Q7sL5VvJC99fRjrYeXZrxPBbJcXZdwtY61F58QYD86MQwpzhEuLGGsseg==
|
integrity sha512-UAOlTdH4WWkpIfi5fAKtLCCj3kb4cecxIhj57tuLORidq0z9VrY+9pFN86yIuLWGJPsaO22B1pLX/mwEDrOJPw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.17.3"
|
"@standardnotes/auth" "^3.17.4"
|
||||||
"@standardnotes/features" "^1.34.4"
|
"@standardnotes/features" "^1.34.5"
|
||||||
|
|
||||||
"@standardnotes/features@1.34.4", "@standardnotes/features@^1.34.4":
|
"@standardnotes/features@1.34.5", "@standardnotes/features@^1.34.5":
|
||||||
version "1.34.4"
|
version "1.34.5"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.34.4.tgz#171ffb481600195c2cb4f8d23e630ce3f840932a"
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.34.5.tgz#3077cf8c6c353694b3a8c0350961a0262f3139e8"
|
||||||
integrity sha512-Ej+9s2H208dF8M/hXKkQ+MzGzM4qioPvfF0wmZ/ovz/PyIkGAOGU/l3zxJPI4vov4W7Xvxk6jMAxa1LEOerDuQ==
|
integrity sha512-b3T67XkMaiNR4D2n6/wszMK+eCEkX6xdYxqCeJYl8ofeM25AtznLMFcRQtCCOoNso+fld8vwCF+VBVp/l5yuDw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.17.3"
|
"@standardnotes/auth" "^3.17.4"
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
|
|
||||||
"@standardnotes/payloads@^1.4.3":
|
"@standardnotes/filepicker@1.8.0":
|
||||||
version "1.4.3"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/payloads/-/payloads-1.4.3.tgz#8d146a61d8bf173835ee54a683d1e6cc95ca15ee"
|
resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.8.0.tgz#f8d85350c4b4022235e3017b0b2c7841882eef4f"
|
||||||
integrity sha512-mwC2EBHjniZBF3eSfkNE45VLGOn0xKWkOcAFb0sSLjouB4Mk90/CG+9gIGVZX8WcpG62A/vVcgvvJtcOtNfK6w==
|
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:
|
dependencies:
|
||||||
"@standardnotes/applications" "^1.1.3"
|
"@standardnotes/applications" "^1.1.3"
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
"@standardnotes/features" "^1.34.4"
|
"@standardnotes/features" "^1.34.5"
|
||||||
"@standardnotes/utils" "^1.2.3"
|
"@standardnotes/utils" "^1.2.3"
|
||||||
|
|
||||||
"@standardnotes/responses@1.3.2", "@standardnotes/responses@^1.3.2":
|
"@standardnotes/responses@1.3.4", "@standardnotes/responses@^1.3.4":
|
||||||
version "1.3.2"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.3.2.tgz#fc967bc8013bc1df3e627094cce7b44e99fb8ecc"
|
resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.3.4.tgz#94dae8bafb481dd9b263f78af7558a2e762cf0ed"
|
||||||
integrity sha512-d7U9IpngnnAxmT0S0uKPdQgklzykBrHZNj2UlcbwkhAjbSf1mB4nPaEQFa9qjl30YeasK+0EbJjf2G8ThCcE8Q==
|
integrity sha512-4oxPppADhKJ2K1X4SGRiUuFfzbYIrK57sNP1V8HJod2ULp4IPPZbkvpSmtVaSUeyPGqbpszSltQBVCblgfe3nQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.17.3"
|
"@standardnotes/auth" "^3.17.4"
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
"@standardnotes/features" "^1.34.4"
|
"@standardnotes/features" "^1.34.5"
|
||||||
"@standardnotes/payloads" "^1.4.3"
|
"@standardnotes/payloads" "^1.4.4"
|
||||||
|
|
||||||
"@standardnotes/services@1.5.4", "@standardnotes/services@^1.5.4":
|
"@standardnotes/services@1.5.6", "@standardnotes/services@^1.5.6":
|
||||||
version "1.5.4"
|
version "1.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.5.4.tgz#16067c9bd0d8a54e5ea2295e97cbbe4327e80811"
|
resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.5.6.tgz#53938d82a8492d88097dfcfb714ca472c8557ab5"
|
||||||
integrity sha512-qyq2KzvDWqUvBLXAdW8KQI1AqpdynSItn3iBzDM+h4U4rJMaAmkPoWtMxVcBAjz7cdHC5xku6t/iAvvKFKqUkA==
|
integrity sha512-2G7lGC+aYO/t0viIhL5EZKneJM+wqRfZNPw83SsjW1YR4hIAWz7C6sUwAnkWorCeiSWQoXRI2O0AhDaCHpVUXg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/applications" "^1.1.3"
|
"@standardnotes/applications" "^1.1.3"
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
"@standardnotes/responses" "^1.3.2"
|
"@standardnotes/responses" "^1.3.4"
|
||||||
"@standardnotes/utils" "^1.2.3"
|
"@standardnotes/utils" "^1.2.3"
|
||||||
|
|
||||||
"@standardnotes/settings@1.12.0", "@standardnotes/settings@^1.12.0":
|
"@standardnotes/settings@1.12.0", "@standardnotes/settings@^1.12.0":
|
||||||
@@ -2396,19 +2401,19 @@
|
|||||||
buffer "^6.0.3"
|
buffer "^6.0.3"
|
||||||
libsodium-wrappers "^0.7.9"
|
libsodium-wrappers "^0.7.9"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.77.2":
|
"@standardnotes/snjs@2.79.0":
|
||||||
version "2.77.2"
|
version "2.79.0"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.77.2.tgz#f9a687a81f84b1978b1edf5e0527f4dc2702bef8"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.79.0.tgz#645f13c068972ce8c90132312e74eeebf9a375bb"
|
||||||
integrity sha512-r5bdOVltdhgJgTI9CrlMoC/jCmvEeLIgkMy5RtT5K6EBOnxu9soxsBA2cvPo6nIs8+m6BS4psLpMBy93Kx/D5w==
|
integrity sha512-fDLuQaAzZrBMjRhhkUy7B8xAzx47prFiLtEPsAJkZNHumkoB1KGU2iEE5ZxnlHc008ilLX0Nj4r5tqGXKxUFQA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/applications" "^1.1.3"
|
"@standardnotes/applications" "^1.1.3"
|
||||||
"@standardnotes/auth" "^3.17.3"
|
"@standardnotes/auth" "^3.17.4"
|
||||||
"@standardnotes/common" "^1.15.3"
|
"@standardnotes/common" "^1.15.3"
|
||||||
"@standardnotes/domain-events" "^2.23.26"
|
"@standardnotes/domain-events" "^2.24.1"
|
||||||
"@standardnotes/features" "^1.34.4"
|
"@standardnotes/features" "^1.34.5"
|
||||||
"@standardnotes/payloads" "^1.4.3"
|
"@standardnotes/payloads" "^1.4.4"
|
||||||
"@standardnotes/responses" "^1.3.2"
|
"@standardnotes/responses" "^1.3.4"
|
||||||
"@standardnotes/services" "^1.5.4"
|
"@standardnotes/services" "^1.5.6"
|
||||||
"@standardnotes/settings" "^1.12.0"
|
"@standardnotes/settings" "^1.12.0"
|
||||||
"@standardnotes/sncrypto-common" "^1.7.3"
|
"@standardnotes/sncrypto-common" "^1.7.3"
|
||||||
"@standardnotes/utils" "^1.2.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"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
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":
|
"@types/ws@^8.2.2":
|
||||||
version "8.2.2"
|
version "8.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21"
|
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21"
|
||||||
|
|||||||
Reference in New Issue
Block a user