feat: file drop handling for super notes (#1990)

This commit is contained in:
Mo
2022-11-09 14:55:44 -06:00
committed by GitHub
parent c961d5e17d
commit fdf9ab0fcb
10 changed files with 111 additions and 25 deletions

View File

@@ -29,6 +29,7 @@ import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
import ApplicationProvider from './ApplicationProvider'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
type Props = {
application: WebApplication
@@ -219,7 +220,9 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
/>
<NoteGroupView application={application} />
<ErrorBoundary>
<NoteGroupView application={application} />
</ErrorBoundary>
</FileDragNDropProvider>
</div>

View File

@@ -9,10 +9,12 @@ import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { LinkingController } from '@/Controllers/LinkingController'
import LinkingControllerProvider from './Contexts/LinkingControllerProvider'
import LinkingControllerProvider from '../../Controllers/LinkingControllerProvider'
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
import ItemBubblePlugin from './Plugins/ItemBubblePlugin/ItemBubblePlugin'
import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlugin'
import { FilesController } from '@/Controllers/FilesController'
import FilesControllerProvider from '@/Controllers/FilesControllerProvider'
const StringEllipses = '...'
const NotePreviewCharLimit = 160
@@ -21,9 +23,10 @@ type Props = {
application: WebApplication
note: SNNote
linkingController: LinkingController
filesController: FilesController
}
export const BlockEditor: FunctionComponent<Props> = ({ note, application, linkingController }) => {
export const BlockEditor: FunctionComponent<Props> = ({ note, application, linkingController, filesController }) => {
const controller = useRef(new BlockEditorController(note, application))
const handleChange = useCallback(
@@ -51,19 +54,21 @@ export const BlockEditor: FunctionComponent<Props> = ({ note, application, linki
<div className="relative h-full w-full p-5">
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<BlocksEditorComposer initialValue={note.text} nodes={[FileNode, BubbleNode]}>
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
>
<ItemSelectionPlugin currentNote={note} />
<FilePlugin />
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
</BlocksEditor>
</BlocksEditorComposer>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer initialValue={note.text} nodes={[FileNode, BubbleNode]}>
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
>
<ItemSelectionPlugin currentNote={note} />
<FilePlugin />
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>
</LinkingControllerProvider>
</ErrorBoundary>
</div>

View File

@@ -6,9 +6,12 @@ import { FileNode } from './Nodes/FileNode'
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_EDITOR } from 'lexical'
import { $createFileNode } from './Nodes/FileUtils'
import { $wrapNodeInElement } from '@lexical/utils'
import { useFilesController } from '@/Controllers/FilesControllerProvider'
import { FilesControllerEvent } from '@/Controllers/FilesController'
export default function FilePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()
const filesController = useFilesController()
useEffect(() => {
if (!editor.hasNodes([FileNode])) {
@@ -19,7 +22,6 @@ export default function FilePlugin(): JSX.Element | null {
INSERT_FILE_COMMAND,
(payload) => {
const fileNode = $createFileNode(payload)
// $insertNodeToNearestRoot(fileNode)
$insertNodes([fileNode])
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
@@ -31,5 +33,16 @@ export default function FilePlugin(): JSX.Element | null {
)
}, [editor])
useEffect(() => {
const disposer = filesController.addEventObserver((event, data) => {
if (event === FilesControllerEvent.FileUploadedToNote) {
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
editor.dispatchCommand(INSERT_FILE_COMMAND, fileUuid)
}
})
return disposer
}, [filesController, editor])
return null
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import LinkedItemBubble from '@/Components/LinkedItems/LinkedItemBubble'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import { useLinkingController } from '@/Components/BlockEditor/Contexts/LinkingControllerProvider'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { useResponsiveAppPane } from '@/Components/ResponsivePane/ResponsivePaneProvider'
import { LexicalNode } from 'lexical'

View File

@@ -9,7 +9,7 @@ import { ContentType, SNNote } from '@standardnotes/snjs'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import Popover from '@/Components/Popover/Popover'
import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands'
import { useLinkingController } from '../../Contexts/LinkingControllerProvider'
import { useLinkingController } from '../../../../Controllers/LinkingControllerProvider'
import { PopoverClassNames } from '../ClassNames'
type Props = {

View File

@@ -8,7 +8,6 @@ import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedIte
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
import { classNames } from '@/Utils/ConcatenateClassNames'
@@ -996,7 +995,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
const editorMode =
featureTrunkEnabled(FeatureTrunkName.Blocks) && this.note.noteType === NoteType.Blocks
this.note.noteType === NoteType.Blocks
? 'blocks'
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
? 'plain'
@@ -1155,6 +1154,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
application={this.application}
note={this.note}
linkingController={this.viewControllerManager.linkingController}
filesController={this.viewControllerManager.filesController}
/>
</div>
)}

View File

@@ -1,11 +1,14 @@
import { CrossControllerEvent } from '../CrossControllerEvent'
import { InternalEventBus, InternalEventPublishStrategy } from '@standardnotes/snjs'
import { InternalEventBus, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
import { WebApplication } from '../../Application/Application'
import { Disposer } from '@/Types/Disposer'
export abstract class AbstractViewController {
type ControllerEventObserver<Event = void, EventData = void> = (event: Event, data: EventData) => void
export abstract class AbstractViewController<Event = void, EventData = void> {
dealloced = false
protected disposers: Disposer[] = []
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
constructor(public application: WebApplication, protected eventBus: InternalEventBus) {}
@@ -23,5 +26,19 @@ export abstract class AbstractViewController {
}
;(this.disposers as unknown) = undefined
this.eventObservers.length = 0
}
addEventObserver(observer: ControllerEventObserver<Event, EventData>): () => void {
this.eventObservers.push(observer)
return () => {
removeFromArray(this.eventObservers, observer)
}
}
notifyEvent(event: Event, data: EventData): void {
this.eventObservers.forEach((observer) => observer(event, data))
}
}

View File

@@ -34,7 +34,17 @@ const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverF
type FileContextMenuLocation = { x: number; y: number }
export class FilesController extends AbstractViewController {
export type FilesControllerEventData = {
[FilesControllerEvent.FileUploadedToNote]: {
uuid: string
}
}
export enum FilesControllerEvent {
FileUploadedToNote,
}
export class FilesController extends AbstractViewController<FilesControllerEvent, FilesControllerEventData> {
allFiles: FileItem[] = []
attachedFiles: FileItem[] = []
showFileContextMenu = false
@@ -388,6 +398,10 @@ export class FilesController extends AbstractViewController {
type: ToastType.Success,
message: `Uploaded file "${uploadedFile.name}"`,
})
this.notifyEvent(FilesControllerEvent.FileUploadedToNote, {
[FilesControllerEvent.FileUploadedToNote]: { uuid: uploadedFile.uuid },
})
}
return uploadedFiles

View File

@@ -0,0 +1,35 @@
import { ReactNode, createContext, useContext, memo } from 'react'
import { observer } from 'mobx-react-lite'
import { FilesController } from '@/Controllers/FilesController'
const FilesControllerContext = createContext<FilesController | undefined>(undefined)
export const useFilesController = () => {
const value = useContext(FilesControllerContext)
if (!value) {
throw new Error('Component must be a child of <FilesControllerProvider />')
}
return value
}
type ChildrenProps = {
children: ReactNode
}
type ProviderProps = {
controller: FilesController
} & ChildrenProps
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
const FilesControllerProvider = ({ controller, children }: ProviderProps) => {
return (
<FilesControllerContext.Provider value={controller}>
<MemoizedChildren children={children} />
</FilesControllerContext.Provider>
)
}
export default observer(FilesControllerProvider)

View File

@@ -1,5 +1,4 @@
import { ReactNode, createContext, useContext, memo } from 'react'
import { observer } from 'mobx-react-lite'
import { LinkingController } from '@/Controllers/LinkingController'