Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: immediately insert file node and show progress inside super note when uploading a new file #2876

Merged
merged 6 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/files/src/Domain/Service/FilesClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface FilesClientInterface {
finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
uuid: string,
): Promise<FileItem | ClientDisplayableError>

downloadFile(
Expand Down
27 changes: 20 additions & 7 deletions packages/services/src/Domain/Files/FileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
isEncryptedPayload,
VaultListingInterface,
SharedVaultListingInterface,
DecryptedPayload,
FillItemContent,
PayloadVaultOverrides,
PayloadTimestampDefaults,
CreateItemFromPayload,
DecryptedItemInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
Expand Down Expand Up @@ -246,6 +252,7 @@ export class FileService extends AbstractService implements FilesClientInterface
public async finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
uuid: string,
): Promise<FileItem | ClientDisplayableError> {
const uploadSessionClosed = await this.api.closeUploadSession(
operation.getValetToken(),
Expand All @@ -268,16 +275,22 @@ export class FileService extends AbstractService implements FilesClientInterface
remoteIdentifier: result.remoteIdentifier,
}

const file = await this.mutator.createItem<FileItem>(
ContentType.TYPES.File,
FillItemContentSpecialized(fileContent),
true,
operation.vault,
)
const filePayload = new DecryptedPayload<FileContent>({
uuid,
content_type: ContentType.TYPES.File,
content: FillItemContent<FileContent>(FillItemContentSpecialized(fileContent)),
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()

return file
return insertedItem
}

private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
Expand Down
23 changes: 20 additions & 3 deletions packages/web/src/javascripts/Components/FileDragNDropProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEf
import Portal from './Portal/Portal'
import { ElementIds } from '@/Constants/ElementIDs'

type FileDragTargetData = {
type FileDragTargetCommonData = {
tooltipText: string
callback: (files: FileItem) => void
note?: SNNote
}

type FileDragTargetCallbacks =
| {
callback: (files: FileItem) => void
handleFileUpload?: never
}
| {
handleFileUpload: (fileOrHandle: File | FileSystemFileHandle) => void
callback?: never
}
type FileDragTargetData = FileDragTargetCommonData & FileDragTargetCallbacks

type FileDnDContextData = {
isDraggingFiles: boolean
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
Expand Down Expand Up @@ -203,6 +213,11 @@ const FileDragNDropProvider = ({ application, children }: Props) => {

const dragTarget = closestDragTarget ? dragTargets.current.get(closestDragTarget) : undefined

if (dragTarget?.handleFileUpload) {
dragTarget.handleFileUpload(fileOrHandle)
return
}

const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle, {
note: dragTarget?.note,
})
Expand All @@ -211,7 +226,9 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
return
}

dragTarget?.callback(uploadedFile)
if (dragTarget?.callback) {
dragTarget.callback(uploadedFile)
}
})

dragCounter.current = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { SNNote } from '@standardnotes/snjs'
import { NoteType, SNNote } from '@standardnotes/snjs'
import { useEffect } from 'react'
import { useApplication } from '../ApplicationProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
Expand All @@ -20,17 +20,28 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file
const target = noteViewElement

if (target) {
addDragTarget(target, {
tooltipText: 'Drop your files to upload and link them to the current note',
callback: async (uploadedFile) => {
await linkingController.linkItems(note, uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = note.protected
})
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
},
note,
})
const tooltipText = 'Drop your files to upload and link them to the current note'
if (note.noteType === NoteType.Super) {
addDragTarget(target, {
tooltipText,
handleFileUpload: (fileOrHandle) => {
filesController.uploadAndInsertFileToCurrentNote(fileOrHandle)
},
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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createCommand, LexicalCommand } from 'lexical'

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_DATETIME_COMMAND: LexicalCommand<'date' | 'time' | 'datetime'> =
createCommand('INSERT_DATETIME_COMMAND')
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { useEffect, useState } from 'react'
Expand All @@ -19,45 +19,24 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { SNNote } from '@standardnotes/snjs'
import Spinner from '../../../Spinner/Spinner'
import Modal from '../../Lexical/UI/Modal'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../../Utils'

export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')

function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
const application = useApplication()
function UploadFileDialog({ onClose }: { onClose: () => void }) {
const [editor] = useLexicalComposerContext()
const filesController = useFilesController()
const linkingController = useLinkingController()

const [file, setFile] = useState<File>()
const [isUploadingFile, setIsUploadingFile] = useState(false)

const onClick = () => {
if (!file) {
return
}

setIsUploadingFile(true)
filesController
.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()
})
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
onClose()
}

return (
Expand All @@ -72,13 +51,9 @@ function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClo
}}
/>
<div className="mt-1.5 flex justify-end">
{isUploadingFile ? (
<Spinner className="h-4 w-4" />
) : (
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
Upload
</Button>
)}
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
Upload
</Button>
</div>
</>
)
Expand All @@ -99,17 +74,23 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS

const uploadFilesList = (files: FileList) => {
Array.from(files).forEach(async (file) => {
try {
const uploadedFile = await filesController.uploadNewFile(file)
if (uploadedFile) {
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
void linkingController.linkItemToSelectedItem(uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = currentNote.protected
})
}
} catch (error) {
console.error(error)
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
})
}

const insertFileNode = (uuid: string, onInsert?: (node: FileNode) => void) => {
editor.update(() => {
const fileNode = $createFileNode(uuid)
$insertNodes([fileNode])
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
}
const newLineNode = $createParagraphNode()
fileNode.getParentOrThrow().insertAfter(newLineNode)
newLineNode.selectEnd()
editor.focus()
if (onInsert) {
onInsert(fileNode)
}
})
}
Expand All @@ -118,14 +99,34 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
editor.registerCommand<string>(
INSERT_FILE_COMMAND,
(payload) => {
const fileNode = $createFileNode(payload)
$insertNodes([fileNode])
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
}
const newLineNode = $createParagraphNode()
fileNode.getParentOrThrow().insertAfter(newLineNode)

insertFileNode(payload)
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
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
},
COMMAND_PRIORITY_EDITOR,
Expand All @@ -150,28 +151,26 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
},
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(() => {
const disposer = filesController.addEventObserver((event, data) => {
if (event === FilesControllerEvent.FileUploadedToNote) {
if (event === FilesControllerEvent.FileUploadedToNote && data[FilesControllerEvent.FileUploadedToNote]) {
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
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)
}
}
})

Expand All @@ -181,7 +180,7 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
if (showFileUploadModal) {
return (
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
<UploadFileDialog onClose={() => setShowFileUploadModal(false)} />
</Modal>
)
}
Expand Down
Loading
Loading