chore: immediately insert file node and show progress inside super note when uploading a new file (#2876) [skip e2e]
This commit is contained in:
@@ -23,6 +23,7 @@ export interface FilesClientInterface {
|
|||||||
finishUpload(
|
finishUpload(
|
||||||
operation: EncryptAndUploadFileOperation,
|
operation: EncryptAndUploadFileOperation,
|
||||||
fileMetadata: FileMetadata,
|
fileMetadata: FileMetadata,
|
||||||
|
uuid: string,
|
||||||
): Promise<FileItem | ClientDisplayableError>
|
): Promise<FileItem | ClientDisplayableError>
|
||||||
|
|
||||||
downloadFile(
|
downloadFile(
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import {
|
|||||||
isEncryptedPayload,
|
isEncryptedPayload,
|
||||||
VaultListingInterface,
|
VaultListingInterface,
|
||||||
SharedVaultListingInterface,
|
SharedVaultListingInterface,
|
||||||
|
DecryptedPayload,
|
||||||
|
FillItemContent,
|
||||||
|
PayloadVaultOverrides,
|
||||||
|
PayloadTimestampDefaults,
|
||||||
|
CreateItemFromPayload,
|
||||||
|
DecryptedItemInterface,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
|
import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
|
||||||
@@ -246,6 +252,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
|||||||
public async finishUpload(
|
public async finishUpload(
|
||||||
operation: EncryptAndUploadFileOperation,
|
operation: EncryptAndUploadFileOperation,
|
||||||
fileMetadata: FileMetadata,
|
fileMetadata: FileMetadata,
|
||||||
|
uuid: string,
|
||||||
): Promise<FileItem | ClientDisplayableError> {
|
): Promise<FileItem | ClientDisplayableError> {
|
||||||
const uploadSessionClosed = await this.api.closeUploadSession(
|
const uploadSessionClosed = await this.api.closeUploadSession(
|
||||||
operation.getValetToken(),
|
operation.getValetToken(),
|
||||||
@@ -268,16 +275,22 @@ export class FileService extends AbstractService implements FilesClientInterface
|
|||||||
remoteIdentifier: result.remoteIdentifier,
|
remoteIdentifier: result.remoteIdentifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await this.mutator.createItem<FileItem>(
|
const filePayload = new DecryptedPayload<FileContent>({
|
||||||
ContentType.TYPES.File,
|
uuid,
|
||||||
FillItemContentSpecialized(fileContent),
|
content_type: ContentType.TYPES.File,
|
||||||
true,
|
content: FillItemContent<FileContent>(FillItemContentSpecialized(fileContent)),
|
||||||
operation.vault,
|
dirty: true,
|
||||||
)
|
...PayloadVaultOverrides(operation.vault),
|
||||||
|
...PayloadTimestampDefaults(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileItem = CreateItemFromPayload(filePayload) as DecryptedItemInterface<FileContent>
|
||||||
|
|
||||||
|
const insertedItem = await this.mutator.insertItem<FileItem>(fileItem)
|
||||||
|
|
||||||
await this.sync.sync()
|
await this.sync.sync()
|
||||||
|
|
||||||
return file
|
return insertedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
|
private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
|
||||||
|
|||||||
@@ -8,12 +8,22 @@ import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEf
|
|||||||
import Portal from './Portal/Portal'
|
import Portal from './Portal/Portal'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
|
||||||
type FileDragTargetData = {
|
type FileDragTargetCommonData = {
|
||||||
tooltipText: string
|
tooltipText: string
|
||||||
callback: (files: FileItem) => void
|
|
||||||
note?: SNNote
|
note?: SNNote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileDragTargetCallbacks =
|
||||||
|
| {
|
||||||
|
callback: (files: FileItem) => void
|
||||||
|
handleFileUpload?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
handleFileUpload: (fileOrHandle: File | FileSystemFileHandle) => void
|
||||||
|
callback?: never
|
||||||
|
}
|
||||||
|
type FileDragTargetData = FileDragTargetCommonData & FileDragTargetCallbacks
|
||||||
|
|
||||||
type FileDnDContextData = {
|
type FileDnDContextData = {
|
||||||
isDraggingFiles: boolean
|
isDraggingFiles: boolean
|
||||||
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
|
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
|
||||||
@@ -203,6 +213,11 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
|
|||||||
|
|
||||||
const dragTarget = closestDragTarget ? dragTargets.current.get(closestDragTarget) : undefined
|
const dragTarget = closestDragTarget ? dragTargets.current.get(closestDragTarget) : undefined
|
||||||
|
|
||||||
|
if (dragTarget?.handleFileUpload) {
|
||||||
|
dragTarget.handleFileUpload(fileOrHandle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle, {
|
const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle, {
|
||||||
note: dragTarget?.note,
|
note: dragTarget?.note,
|
||||||
})
|
})
|
||||||
@@ -211,7 +226,9 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dragTarget?.callback(uploadedFile)
|
if (dragTarget?.callback) {
|
||||||
|
dragTarget.callback(uploadedFile)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
dragCounter.current = 0
|
dragCounter.current = 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FilesController } from '@/Controllers/FilesController'
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { SNNote } from '@standardnotes/snjs'
|
import { NoteType, SNNote } from '@standardnotes/snjs'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
import { useFileDragNDrop } from '../FileDragNDropProvider'
|
||||||
@@ -20,17 +20,28 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file
|
|||||||
const target = noteViewElement
|
const target = noteViewElement
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
addDragTarget(target, {
|
const tooltipText = 'Drop your files to upload and link them to the current note'
|
||||||
tooltipText: 'Drop your files to upload and link them to the current note',
|
if (note.noteType === NoteType.Super) {
|
||||||
callback: async (uploadedFile) => {
|
addDragTarget(target, {
|
||||||
await linkingController.linkItems(note, uploadedFile)
|
tooltipText,
|
||||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
handleFileUpload: (fileOrHandle) => {
|
||||||
mutator.protected = note.protected
|
filesController.uploadAndInsertFileToCurrentNote(fileOrHandle)
|
||||||
})
|
},
|
||||||
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
|
note,
|
||||||
},
|
})
|
||||||
note,
|
} else {
|
||||||
})
|
addDragTarget(target, {
|
||||||
|
tooltipText,
|
||||||
|
callback: async (uploadedFile) => {
|
||||||
|
await linkingController.linkItems(note, uploadedFile)
|
||||||
|
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||||
|
mutator.protected = note.protected
|
||||||
|
})
|
||||||
|
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
|
||||||
|
},
|
||||||
|
note,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createCommand, LexicalCommand } from 'lexical'
|
import { createCommand, LexicalCommand } from 'lexical'
|
||||||
|
|
||||||
export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand('INSERT_FILE_COMMAND')
|
export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand('INSERT_FILE_COMMAND')
|
||||||
|
export const UPLOAD_AND_INSERT_FILE_COMMAND: LexicalCommand<File> = createCommand('UPLOAD_AND_INSERT_FILE_COMMAND')
|
||||||
export const INSERT_BUBBLE_COMMAND: LexicalCommand<string> = createCommand('INSERT_BUBBLE_COMMAND')
|
export const INSERT_BUBBLE_COMMAND: LexicalCommand<string> = createCommand('INSERT_BUBBLE_COMMAND')
|
||||||
export const INSERT_DATETIME_COMMAND: LexicalCommand<'date' | 'time' | 'datetime'> =
|
export const INSERT_DATETIME_COMMAND: LexicalCommand<'date' | 'time' | 'datetime'> =
|
||||||
createCommand('INSERT_DATETIME_COMMAND')
|
createCommand('INSERT_DATETIME_COMMAND')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { INSERT_FILE_COMMAND } from '../Commands'
|
import { INSERT_FILE_COMMAND, UPLOAD_AND_INSERT_FILE_COMMAND } from '../Commands'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -19,45 +19,24 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
|
|||||||
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
||||||
import { useApplication } from '@/Components/ApplicationProvider'
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
import { SNNote } from '@standardnotes/snjs'
|
import { SNNote } from '@standardnotes/snjs'
|
||||||
import Spinner from '../../../Spinner/Spinner'
|
|
||||||
import Modal from '../../Lexical/UI/Modal'
|
import Modal from '../../Lexical/UI/Modal'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import { isMobileScreen } from '../../../../Utils'
|
import { isMobileScreen } from '../../../../Utils'
|
||||||
|
|
||||||
export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')
|
export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')
|
||||||
|
|
||||||
function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
|
function UploadFileDialog({ onClose }: { onClose: () => void }) {
|
||||||
const application = useApplication()
|
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const filesController = useFilesController()
|
|
||||||
const linkingController = useLinkingController()
|
|
||||||
|
|
||||||
const [file, setFile] = useState<File>()
|
const [file, setFile] = useState<File>()
|
||||||
const [isUploadingFile, setIsUploadingFile] = useState(false)
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploadingFile(true)
|
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||||
filesController
|
onClose()
|
||||||
.uploadNewFile(file)
|
|
||||||
.then((uploadedFile) => {
|
|
||||||
if (!uploadedFile) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
|
|
||||||
void linkingController.linkItemToSelectedItem(uploadedFile)
|
|
||||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
|
||||||
mutator.protected = currentNote.protected
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
.finally(() => {
|
|
||||||
setIsUploadingFile(false)
|
|
||||||
onClose()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,13 +51,9 @@ function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClo
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mt-1.5 flex justify-end">
|
<div className="mt-1.5 flex justify-end">
|
||||||
{isUploadingFile ? (
|
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
|
||||||
<Spinner className="h-4 w-4" />
|
Upload
|
||||||
) : (
|
</Button>
|
||||||
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -99,17 +74,23 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
|
|
||||||
const uploadFilesList = (files: FileList) => {
|
const uploadFilesList = (files: FileList) => {
|
||||||
Array.from(files).forEach(async (file) => {
|
Array.from(files).forEach(async (file) => {
|
||||||
try {
|
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||||
const uploadedFile = await filesController.uploadNewFile(file)
|
})
|
||||||
if (uploadedFile) {
|
}
|
||||||
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
|
|
||||||
void linkingController.linkItemToSelectedItem(uploadedFile)
|
const insertFileNode = (uuid: string, onInsert?: (node: FileNode) => void) => {
|
||||||
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
editor.update(() => {
|
||||||
mutator.protected = currentNote.protected
|
const fileNode = $createFileNode(uuid)
|
||||||
})
|
$insertNodes([fileNode])
|
||||||
}
|
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
|
||||||
} catch (error) {
|
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
|
||||||
console.error(error)
|
}
|
||||||
|
const newLineNode = $createParagraphNode()
|
||||||
|
fileNode.getParentOrThrow().insertAfter(newLineNode)
|
||||||
|
newLineNode.selectEnd()
|
||||||
|
editor.focus()
|
||||||
|
if (onInsert) {
|
||||||
|
onInsert(fileNode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -118,14 +99,34 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
editor.registerCommand<string>(
|
editor.registerCommand<string>(
|
||||||
INSERT_FILE_COMMAND,
|
INSERT_FILE_COMMAND,
|
||||||
(payload) => {
|
(payload) => {
|
||||||
const fileNode = $createFileNode(payload)
|
insertFileNode(payload)
|
||||||
$insertNodes([fileNode])
|
return true
|
||||||
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
|
},
|
||||||
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
|
COMMAND_PRIORITY_EDITOR,
|
||||||
}
|
),
|
||||||
const newLineNode = $createParagraphNode()
|
editor.registerCommand(
|
||||||
fileNode.getParentOrThrow().insertAfter(newLineNode)
|
UPLOAD_AND_INSERT_FILE_COMMAND,
|
||||||
|
(file) => {
|
||||||
|
const note = currentNote
|
||||||
|
let fileNode: FileNode | undefined
|
||||||
|
filesController
|
||||||
|
.uploadNewFile(file, {
|
||||||
|
showToast: false,
|
||||||
|
onUploadStart(fileUuid) {
|
||||||
|
insertFileNode(fileUuid, (node) => (fileNode = node))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((uploadedFile) => {
|
||||||
|
if (uploadedFile) {
|
||||||
|
void linkingController.linkItems(note, uploadedFile)
|
||||||
|
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
|
||||||
|
mutator.protected = note.protected
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
editor.update(() => fileNode?.remove())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
@@ -150,28 +151,26 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
},
|
},
|
||||||
COMMAND_PRIORITY_NORMAL,
|
COMMAND_PRIORITY_NORMAL,
|
||||||
),
|
),
|
||||||
editor.registerNodeTransform(FileNode, (node) => {
|
|
||||||
/**
|
|
||||||
* When adding the node we wrap it with a paragraph to avoid insertion errors,
|
|
||||||
* however that causes issues with selection. We unwrap the node to fix that.
|
|
||||||
*/
|
|
||||||
const parent = node.getParent()
|
|
||||||
if (!parent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (parent.getChildrenSize() === 1) {
|
|
||||||
parent.insertBefore(node)
|
|
||||||
parent.remove()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}, [application, currentNote.protected, editor, filesController, linkingController])
|
}, [application, currentNote, editor, filesController, linkingController])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disposer = filesController.addEventObserver((event, data) => {
|
const disposer = filesController.addEventObserver((event, data) => {
|
||||||
if (event === FilesControllerEvent.FileUploadedToNote) {
|
if (event === FilesControllerEvent.FileUploadedToNote && data[FilesControllerEvent.FileUploadedToNote]) {
|
||||||
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
|
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
|
||||||
editor.dispatchCommand(INSERT_FILE_COMMAND, fileUuid)
|
editor.dispatchCommand(INSERT_FILE_COMMAND, fileUuid)
|
||||||
|
} else if (event === FilesControllerEvent.UploadAndInsertFile && data[FilesControllerEvent.UploadAndInsertFile]) {
|
||||||
|
const { fileOrHandle } = data[FilesControllerEvent.UploadAndInsertFile]
|
||||||
|
if (fileOrHandle instanceof FileSystemFileHandle) {
|
||||||
|
fileOrHandle
|
||||||
|
.getFile()
|
||||||
|
.then((file) => {
|
||||||
|
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
} else {
|
||||||
|
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, fileOrHandle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
|||||||
if (showFileUploadModal) {
|
if (showFileUploadModal) {
|
||||||
return (
|
return (
|
||||||
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
|
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
|
||||||
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
|
<UploadFileDialog onClose={() => setShowFileUploadModal(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import FilePreview from '@/Components/FilePreview/FilePreview'
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import Spinner from '@/Components/Spinner/Spinner'
|
||||||
|
import { FilesControllerEvent } from '@/Controllers/FilesController'
|
||||||
|
|
||||||
export type FileComponentProps = Readonly<{
|
export type FileComponentProps = Readonly<{
|
||||||
className: Readonly<{
|
className: Readonly<{
|
||||||
@@ -19,10 +22,11 @@ export type FileComponentProps = Readonly<{
|
|||||||
setZoomLevel: (zoomLevel: number) => void
|
setZoomLevel: (zoomLevel: number) => void
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) {
|
function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const file = useMemo(() => application.items.findItem<FileItem>(fileUuid), [application, fileUuid])
|
const [file, setFile] = useState(() => application.items.findItem<FileItem>(fileUuid))
|
||||||
|
const uploadProgress = application.filesController.uploadProgressMap.get(fileUuid)
|
||||||
|
|
||||||
const [canLoad, setCanLoad] = useState(false)
|
const [canLoad, setCanLoad] = useState(false)
|
||||||
|
|
||||||
@@ -90,6 +94,41 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
|
|||||||
)
|
)
|
||||||
}, [editor, isSelected, nodeKey, setSelected])
|
}, [editor, isSelected, nodeKey, setSelected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return application.filesController.addEventObserver((event, data) => {
|
||||||
|
if (event === FilesControllerEvent.FileUploadFinished && data[FilesControllerEvent.FileUploadFinished]) {
|
||||||
|
const { uploadedFile } = data[FilesControllerEvent.FileUploadFinished]
|
||||||
|
if (uploadedFile.uuid === fileUuid) {
|
||||||
|
setFile(uploadedFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [application.filesController, fileUuid])
|
||||||
|
|
||||||
|
if (uploadProgress && (uploadProgress.progress < 100 || !file)) {
|
||||||
|
const progress = uploadProgress.progress
|
||||||
|
return (
|
||||||
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 p-4 text-center" ref={blockWrapperRef}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Spinner className="h-4 w-4" />
|
||||||
|
Uploading file "{uploadProgress.file.name}"... ({progress}%)
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-[50%] overflow-hidden rounded bg-contrast">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded rounded-tl-none bg-info transition-[width] duration-100"
|
||||||
|
role="progressbar"
|
||||||
|
style={{
|
||||||
|
width: `${progress}%`,
|
||||||
|
}}
|
||||||
|
aria-valuenow={progress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BlockWithAlignableContents>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return (
|
return (
|
||||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
@@ -114,3 +153,5 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
|
|||||||
</BlockWithAlignableContents>
|
</BlockWithAlignableContents>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default observer(FileComponent)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
|
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
|
||||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
import { $createFileNode, convertToFileElement } from './FileUtils'
|
import { $createFileNode, convertToFileElement } from './FileUtils'
|
||||||
import { FileComponent } from './FileComponent'
|
import FileComponent from './FileComponent'
|
||||||
import { SerializedFileNode } from './SerializedFileNode'
|
import { SerializedFileNode } from './SerializedFileNode'
|
||||||
import { ItemNodeInterface } from '../../ItemNodeInterface'
|
import { ItemNodeInterface } from '../../ItemNodeInterface'
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
ProtectionsClientInterface,
|
ProtectionsClientInterface,
|
||||||
SNNote,
|
SNNote,
|
||||||
SyncServiceInterface,
|
SyncServiceInterface,
|
||||||
|
UuidGenerator,
|
||||||
VaultServiceInterface,
|
VaultServiceInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast'
|
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast'
|
||||||
@@ -52,14 +53,22 @@ const NonMutatingFileActions = [FileItemActionType.DownloadFile, FileItemActionT
|
|||||||
|
|
||||||
type FileContextMenuLocation = { x: number; y: number }
|
type FileContextMenuLocation = { x: number; y: number }
|
||||||
|
|
||||||
export type FilesControllerEventData = {
|
export enum FilesControllerEvent {
|
||||||
[FilesControllerEvent.FileUploadedToNote]: {
|
FileUploadedToNote = 'FileUploadedToNote',
|
||||||
uuid: string
|
FileUploadFinished = 'FileUploadFinished',
|
||||||
}
|
UploadAndInsertFile = 'UploadAndInsertFile',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FilesControllerEvent {
|
export type FilesControllerEventData = {
|
||||||
FileUploadedToNote,
|
[FilesControllerEvent.FileUploadedToNote]?: {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
[FilesControllerEvent.FileUploadFinished]?: {
|
||||||
|
uploadedFile: FileItem
|
||||||
|
}
|
||||||
|
[FilesControllerEvent.UploadAndInsertFile]?: {
|
||||||
|
fileOrHandle: File | FileSystemFileHandle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FilesController extends AbstractViewController<FilesControllerEvent, FilesControllerEventData> {
|
export class FilesController extends AbstractViewController<FilesControllerEvent, FilesControllerEventData> {
|
||||||
@@ -73,6 +82,14 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
reader = this.shouldUseStreamingAPI ? StreamingFileReader : ClassicFileReader
|
reader = this.shouldUseStreamingAPI ? StreamingFileReader : ClassicFileReader
|
||||||
maxFileSize = this.reader.maximumFileSize()
|
maxFileSize = this.reader.maximumFileSize()
|
||||||
|
|
||||||
|
uploadProgressMap: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
file: File
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
> = new Map()
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.notesController as unknown) = undefined
|
;(this.notesController as unknown) = undefined
|
||||||
@@ -111,6 +128,8 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
setShowFileContextMenu: action,
|
setShowFileContextMenu: action,
|
||||||
setShowProtectedOverlay: action,
|
setShowProtectedOverlay: action,
|
||||||
setFileContextMenuLocation: action,
|
setFileContextMenuLocation: action,
|
||||||
|
|
||||||
|
uploadProgressMap: observable,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
@@ -453,9 +472,11 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
options: {
|
options: {
|
||||||
showToast?: boolean
|
showToast?: boolean
|
||||||
note?: SNNote
|
note?: SNNote
|
||||||
|
onUploadStart?: (fileUuid: string) => void
|
||||||
|
onUploadFinish?: () => void
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<FileItem | undefined> {
|
): Promise<FileItem | undefined> {
|
||||||
const { showToast = true, note } = options
|
const { showToast = true, note, onUploadStart, onUploadFinish } = options
|
||||||
|
|
||||||
let toastId: string | undefined
|
let toastId: string | undefined
|
||||||
let canShowProgressNotification = false
|
let canShowProgressNotification = false
|
||||||
@@ -482,6 +503,17 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uuid = UuidGenerator.GenerateUuid()
|
||||||
|
|
||||||
|
this.uploadProgressMap.set(uuid, {
|
||||||
|
file: fileToUpload,
|
||||||
|
progress: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (onUploadStart) {
|
||||||
|
onUploadStart(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
const vaultForNote = note ? this.vaults.getItemVault(note) : undefined
|
const vaultForNote = note ? this.vaults.getItemVault(note) : undefined
|
||||||
|
|
||||||
const operation = await this.files.beginNewFileUpload(
|
const operation = await this.files.beginNewFileUpload(
|
||||||
@@ -499,6 +531,11 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
|
|
||||||
const initialProgress = operation.getProgress().percentComplete
|
const initialProgress = operation.getProgress().percentComplete
|
||||||
|
|
||||||
|
this.uploadProgressMap.set(uuid, {
|
||||||
|
file: fileToUpload,
|
||||||
|
progress: initialProgress,
|
||||||
|
})
|
||||||
|
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
if (this.mobileDevice && canShowProgressNotification) {
|
if (this.mobileDevice && canShowProgressNotification) {
|
||||||
toastId = await this.mobileDevice.displayNotification({
|
toastId = await this.mobileDevice.displayNotification({
|
||||||
@@ -521,6 +558,10 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
await this.files.pushBytesForUpload(operation, data, index, isLast)
|
await this.files.pushBytesForUpload(operation, data, index, isLast)
|
||||||
|
|
||||||
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
const percentComplete = Math.round(operation.getProgress().percentComplete)
|
||||||
|
this.uploadProgressMap.set(uuid, {
|
||||||
|
file: fileToUpload,
|
||||||
|
progress: percentComplete,
|
||||||
|
})
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
if (this.mobileDevice && canShowProgressNotification) {
|
if (this.mobileDevice && canShowProgressNotification) {
|
||||||
await this.mobileDevice.displayNotification({
|
await this.mobileDevice.displayNotification({
|
||||||
@@ -547,7 +588,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
fileResult.mimeType = await this.archiveService.getMimeType(ext)
|
fileResult.mimeType = await this.archiveService.getMimeType(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFile = await this.files.finishUpload(operation, fileResult)
|
const uploadedFile = await this.files.finishUpload(operation, fileResult, uuid)
|
||||||
|
|
||||||
if (uploadedFile instanceof ClientDisplayableError) {
|
if (uploadedFile instanceof ClientDisplayableError) {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -557,6 +598,14 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
throw new Error(uploadedFile.text)
|
throw new Error(uploadedFile.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onUploadFinish) {
|
||||||
|
onUploadFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyEvent(FilesControllerEvent.FileUploadFinished, {
|
||||||
|
[FilesControllerEvent.FileUploadFinished]: { uploadedFile },
|
||||||
|
})
|
||||||
|
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
if (this.mobileDevice && canShowProgressNotification) {
|
if (this.mobileDevice && canShowProgressNotification) {
|
||||||
this.mobileDevice.cancelNotification(toastId).catch(console.error)
|
this.mobileDevice.cancelNotification(toastId).catch(console.error)
|
||||||
@@ -635,6 +684,12 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadAndInsertFileToCurrentNote(fileOrHandle: File | FileSystemFileHandle) {
|
||||||
|
this.notifyEvent(FilesControllerEvent.UploadAndInsertFile, {
|
||||||
|
[FilesControllerEvent.UploadAndInsertFile]: { fileOrHandle },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
deleteFilesPermanently = async (files: FileItem[]) => {
|
deleteFilesPermanently = async (files: FileItem[]) => {
|
||||||
const title = Strings.trashItemsTitle
|
const title = Strings.trashItemsTitle
|
||||||
const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles
|
const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles
|
||||||
|
|||||||
Reference in New Issue
Block a user