Skip to content

Commit

Permalink
file upload progress in super embed
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara committed May 5, 2024
1 parent 5cb781e commit 43494a2
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 68 deletions.
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
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,21 @@ 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)
if (onInsert) {
onInsert(fileNode)
}
})
}
Expand All @@ -118,14 +97,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 {
fileNode?.remove()
}
})
.catch(console.error)
return true
},
COMMAND_PRIORITY_EDITOR,
Expand Down Expand Up @@ -159,17 +158,20 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
if (!parent) {
return
}
// if ($isRootOrShadowRoot(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)
}
Expand All @@ -181,7 +183,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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import FilePreview from '@/Components/FilePreview/FilePreview'
import { FileItem } from '@standardnotes/snjs'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
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<{
className: Readonly<{
Expand All @@ -19,10 +22,11 @@ export type FileComponentProps = Readonly<{
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 [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)

Expand Down Expand Up @@ -90,6 +94,41 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
)
}, [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) {
return (
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
Expand All @@ -114,3 +153,5 @@ export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel,
</BlockWithAlignableContents>
)
}

export default observer(FileComponent)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import { $createFileNode, convertToFileElement } from './FileUtils'
import { FileComponent } from './FileComponent'
import FileComponent from './FileComponent'
import { SerializedFileNode } from './SerializedFileNode'
import { ItemNodeInterface } from '../../ItemNodeInterface'

Expand Down
Loading

0 comments on commit 43494a2

Please sign in to comment.