diff --git a/pkg/interface/.eslintrc.js b/pkg/interface/.eslintrc.js index bcdb06a01c..09212671c9 100644 --- a/pkg/interface/.eslintrc.js +++ b/pkg/interface/.eslintrc.js @@ -2,5 +2,11 @@ module.exports = { extends: '@urbit', env: { 'jest': true + }, + rules: { + // Because we use styled system, and use + // the convention of each prop on a new line + // we probably shouldn't keep this on + 'max-lines-per-function': ['off', {}] } }; diff --git a/pkg/interface/src/logic/lib/useFileUpload.ts b/pkg/interface/src/logic/lib/useFileUpload.ts new file mode 100644 index 0000000000..0ce14f9120 --- /dev/null +++ b/pkg/interface/src/logic/lib/useFileUpload.ts @@ -0,0 +1,77 @@ +import _ from 'lodash'; +import { useState, ClipboardEvent } from 'react'; +import { useFileDrag } from './useDrag'; +import useStorage, { IuseStorage } from './useStorage'; + +export type FileUploadSource = 'drag' | 'paste' | 'direct'; + +interface FileUploadEventHandlers { + onSuccess: (url: string, source: FileUploadSource) => void; + onError?: (error: Error) => void; +} + +interface FileUploadHandler { + onFiles: ( + files: FileList | File[], + storage?: IuseStorage, + uploadSource?: FileUploadSource + ) => void | Promise; +} + +function isFileUploadHandler(obj: any): obj is FileUploadHandler { + return typeof obj.onFiles === 'function'; +} + +type useFileUploadParams = { + multiple?: boolean; +} & (FileUploadEventHandlers | FileUploadHandler) + +export function useFileUpload({ multiple = true, ...params }: useFileUploadParams) { + const storage = useStorage(); + const { + canUpload, uploadDefault + } = storage; + const [source, setSource] = useState('paste'); + const drag = useFileDrag(f => uploadFiles(f, 'drag')); + + function onPaste(event: ClipboardEvent) { + if (!event.clipboardData || !event.clipboardData.files.length) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + uploadFiles(event.clipboardData.files, 'paste'); + } + + function uploadFiles(files: FileList | File[], uploadSource: FileUploadSource) { + if (isFileUploadHandler(params)) { + return params.onFiles(files, storage, uploadSource); + } + + if (!canUpload) { + return; + } + + setSource(uploadSource); + + const { onSuccess, onError } = params as FileUploadEventHandlers; + const fileArray = Array.from(files); + const toUpload = multiple ? fileArray : _.take(fileArray); + toUpload.forEach((file) => { + uploadDefault(file) + .then(url => onSuccess(url, source)) + .catch((err: Error) => { + console.log(err); + onError && onError(err); + }); + }); + } + + return { + ...storage, + onPaste, + drag + }; +} diff --git a/pkg/interface/src/logic/lib/useLocalStorageState.ts b/pkg/interface/src/logic/lib/useLocalStorageState.ts index e07db8d378..396f010c30 100644 --- a/pkg/interface/src/logic/lib/useLocalStorageState.ts +++ b/pkg/interface/src/logic/lib/useLocalStorageState.ts @@ -1,6 +1,6 @@ import { useMemo, useEffect, useState } from 'react'; -function retrieve(key: string, initial: T): T { +export function retrieve(key: string, initial: T): T { const s = localStorage.getItem(key); if (s) { try { diff --git a/pkg/interface/src/views/apps/chat/components/ChatAvatar.tsx b/pkg/interface/src/views/apps/chat/components/ChatAvatar.tsx new file mode 100644 index 0000000000..1e6a3ec656 --- /dev/null +++ b/pkg/interface/src/views/apps/chat/components/ChatAvatar.tsx @@ -0,0 +1,50 @@ +import { BaseImage, Box } from '@tlon/indigo-react'; +import { Contact } from '@urbit/api'; +import React from 'react'; +import { Sigil } from '~/logic/lib/sigil'; +import { uxToHex } from '~/logic/lib/util'; + +interface ChatAvatar { + hideAvatars: boolean; + contact?: Contact; +} + +export function ChatAvatar({ contact, hideAvatars }) { + const color = contact ? uxToHex(contact.color) : '000000'; + const sigilClass = contact ? '' : 'mix-blend-diff'; + + if (contact && contact?.avatar && !hideAvatars) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx b/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx index cf9cf0340a..d23bd458b3 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx @@ -4,11 +4,14 @@ import 'codemirror/addon/display/placeholder'; import 'codemirror/addon/hint/show-hint'; import 'codemirror/lib/codemirror.css'; import 'codemirror/mode/markdown/markdown'; -import React, { Component } from 'react'; +import React, { useRef, ClipboardEvent, useEffect, useImperativeHandle } from 'react'; import { UnControlled as CodeEditor } from 'react-codemirror2'; import styled from 'styled-components'; import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util'; import '../css/custom.css'; +import { useChatStore } from './ChatPane'; + +const isMobile = Boolean(MOBILE_BROWSER_REGEX.test(navigator.userAgent)); const MARKDOWN_CONFIG = { name: 'markdown', @@ -30,6 +33,18 @@ const MARKDOWN_CONFIG = { } }; +const defaultOptions = { + mode: MARKDOWN_CONFIG, + lineNumbers: false, + lineWrapping: true, + scrollbarStyle: 'native', + cursorHeight: 0.85, + // The below will ony work once codemirror's bug is fixed + spellcheck: isMobile, + autocorrect: isMobile, + autocapitalize: isMobile +}; + // Until CodeMirror supports options.inputStyle = 'textarea' on mobile, // we need to hack this into a regular input that has some funny behaviors const inputProxy = input => new Proxy(input, { @@ -95,20 +110,13 @@ const MobileBox = styled(Box)` `; interface ChatEditorProps { - message: string; inCodeMode: boolean; - submit: (message: string) => void; - onUnmount: (message: string) => void; - onPaste: () => void; - changeEvent: (message: string) => void; placeholder: string; + submit: (message: string) => void; + onPaste: (codemirrorInstance, event: ClipboardEvent) => void; } -interface ChatEditorState { - message: string; -} - -interface CodeMirrorShim { +export interface CodeMirrorShim { setValue: (string) => void; setOption: (option: string, property: any) => void; focus: () => void; @@ -118,192 +126,143 @@ interface CodeMirrorShim { element: HTMLElement; } -export default class ChatEditor extends Component { - editor: CodeMirrorShim; - constructor(props: ChatEditorProps) { - super(props); +const ChatEditor = React.forwardRef(({ inCodeMode, placeholder, submit, onPaste }, ref) => { + const editorRef = useRef(null); + useImperativeHandle(ref, () => editorRef.current); + const editor = editorRef.current; - this.state = { - message: props.message - }; + const { + message, + setMessage + } = useChatStore(); - this.editor = null; - this.onKeyPress = this.onKeyPress.bind(this); - } - - componentDidMount() { - document.addEventListener('keydown', this.onKeyPress); - } - - componentWillUnmount(): void { - this.props.onUnmount(this.state.message); - document.removeEventListener('keydown', this.onKeyPress); - } - - onKeyPress(e) { + function onKeyPress(e) { const focusedTag = document.activeElement?.nodeName?.toLowerCase(); const shouldCapture = !(focusedTag === 'textarea' || focusedTag === 'input' || e.metaKey || e.ctrlKey); if(/^[a-z]|[A-Z]$/.test(e.key) && shouldCapture) { - this.editor.focus(); + editor.focus(); } if(e.key === 'Escape') { - this.editor.getInputField().blur(); + editor.getInputField().blur(); } } - componentDidUpdate(prevProps: ChatEditorProps): void { - const { props } = this; + useEffect(() => { + document.addEventListener('keydown', onKeyPress); - if (prevProps.message !== props.message && this.editor) { - this.editor.setValue(props.message); - this.editor.setOption('mode', MARKDOWN_CONFIG); - //this.editor?.focus(); - //this.editor.execCommand('goDocEnd'); - //this.editor?.focus(); + return () => { + document.removeEventListener('keydown', onKeyPress); + }; + }, []); + + useEffect(() => { + if (!editor) { return; } - if (!props.inCodeMode) { - this.editor.setOption('mode', MARKDOWN_CONFIG); - this.editor.setOption('placeholder', this.props.placeholder); + if (inCodeMode) { + editor.setOption('mode', null); + editor.setOption('placeholder', 'Code...'); } else { - this.editor.setOption('mode', null); - this.editor.setOption('placeholder', 'Code...'); + editor.setOption('mode', MARKDOWN_CONFIG); + editor.setOption('placeholder', placeholder); } - const value = this.editor.getValue(); // Force redraw of placeholder + const value = editor.getValue(); if(value.length === 0) { - this.editor.setValue(' '); - this.editor.setValue(''); + editor.setValue(' '); + editor.setValue(''); } - } + }, [inCodeMode, placeholder]); - submit() { - if(!this.editor) { - return; - } - - const editorMessage = this.editor.getValue(); - if (editorMessage === '') { - return; - } - - this.setState({ message: '' }); - this.props.submit(editorMessage); - this.editor.setValue(''); - } - - messageChange(editor, data, value) { + function messageChange(editor, data, value) { if(value.endsWith('/')) { editor.showHint(['test', 'foo']); } - if (this.state.message !== '' && value == '') { - this.props.changeEvent(value); - this.setState({ - message: value - }); + if (message !== '' && value == '') { + setMessage(value); } - if (value == this.props.message || value == '' || value == ' ') { + if (value == message || value == '' || value == ' ') { return; } - this.props.changeEvent(value); - this.setState({ - message: value - }); + + setMessage(value); } - render() { - const { - inCodeMode, - placeholder, - message, - ...props - } = this.props; + const codeTheme = inCodeMode ? ' code' : ''; + const options = { + ...defaultOptions, + theme: 'tlon' + codeTheme, + placeholder: inCodeMode ? 'Code...' : placeholder, + extraKeys: { + 'Enter': submit, + 'Esc': () => { + editor?.getInputField().blur(); + } + } + }; - const codeTheme = inCodeMode ? ' code' : ''; - - const options = { - mode: MARKDOWN_CONFIG, - theme: 'tlon' + codeTheme, - lineNumbers: false, - lineWrapping: true, - scrollbarStyle: 'native', - cursorHeight: 0.85, - placeholder: inCodeMode ? 'Code...' : placeholder, - extraKeys: { - 'Enter': () => { - this.submit(); - }, - 'Esc': () => { - this.editor?.getInputField().blur(); - } - }, - // The below will ony work once codemirror's bug is fixed - spellcheck: Boolean(MOBILE_BROWSER_REGEX.test(navigator.userAgent)), - autocorrect: Boolean(MOBILE_BROWSER_REGEX.test(navigator.userAgent)), - autocapitalize: Boolean(MOBILE_BROWSER_REGEX.test(navigator.userAgent)) - }; - - return ( - - {MOBILE_BROWSER_REGEX.test(navigator.userAgent) - ? { - if (this.editor) { - this.editor.element.focus(); - } - }} - > - - this.messageChange(null, null, event.target.value) + return ( + + {isMobile + ? { + if (editor) { + editor.element.focus(); } - onKeyDown={event => - this.messageChange(null, null, (event.target as any).value) - } - ref={(input) => { - if (!input) -return; - this.editor = inputProxy(input); - }} - {...props} - /> - - : this.messageChange(e, d, v)} - editorDidMount={(editor) => { - this.editor = editor; - }} - {...props} - /> - } + }} + > + + messageChange(null, null, event.target.value) + } + onKeyDown={event => + messageChange(null, null, (event.target as any).value) + } + onPaste={e => onPaste(null, e)} + ref={(input) => { + if (!input) + return; + editorRef.current = inputProxy(input); + }} + /> + + : messageChange(e, d, v)} + editorDidMount={(codeEditor) => { + editorRef.current = codeEditor; + }} + onPaste={onPaste as any} + /> + } - - ); - } -} + + ); +}); + +export default ChatEditor; diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index c8194620b0..bca329c04d 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -1,247 +1,166 @@ -import { BaseImage, Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react'; +import { Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react'; import { Contact, Content, evalCord } from '@urbit/api'; -import React, { Component, ReactNode } from 'react'; -import { Sigil } from '~/logic/lib/sigil'; +import React, { FC, PropsWithChildren, useRef, useState } from 'react'; import tokenizeMessage from '~/logic/lib/tokenizeMessage'; import { IuseStorage } from '~/logic/lib/useStorage'; -import { MOBILE_BROWSER_REGEX, uxToHex } from '~/logic/lib/util'; +import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util'; import { withLocalState } from '~/logic/state/local'; -import withStorage from '~/views/components/withStorage'; -import ChatEditor from './ChatEditor'; +import ChatEditor, { CodeMirrorShim } from './ChatEditor'; import airlock from '~/logic/api'; +import { ChatAvatar } from './ChatAvatar'; +import { useChatStore } from './ChatPane'; +import { useImperativeHandle } from 'react'; +import { FileUploadSource, useFileUpload } from '~/logic/lib/useFileUpload'; -type ChatInputProps = IuseStorage & { - ourContact?: Contact; - onUnmount(msg: string): void; - placeholder: string; - message: string; - deleteMessage(): void; +type ChatInputProps = PropsWithChildren void; - children?: ReactNode; -}; +}>; -interface ChatInputState { - inCodeMode: boolean; - submitFocus: boolean; - uploadingPaste: boolean; - currentInput: string; -} +const InputBox: FC = ({ children }) => ( + + { children } + +); -export class ChatInput extends Component { - private chatEditor: React.RefObject; +const IconBox = ({ children, ...props }) => ( + + { children } + +); - constructor(props) { - super(props); +const MobileSubmitButton = ({ enabled, onSubmit }) => ( + onSubmit()} + > + + +); - this.state = { - inCodeMode: false, - submitFocus: false, - uploadingPaste: false, - currentInput: props.message - }; +export const ChatInput = React.forwardRef(({ ourContact, hideAvatars, placeholder, onSubmit }: ChatInputProps, ref) => { + const chatEditor = useRef(null); + useImperativeHandle(ref, () => chatEditor.current); + const [inCodeMode, setInCodeMode] = useState(false); - this.chatEditor = React.createRef(); + const { + message, + setMessage + } = useChatStore(); + const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({ + onSuccess: uploadSuccess + }); - this.submit = this.submit.bind(this); - this.toggleCode = this.toggleCode.bind(this); - this.uploadSuccess = this.uploadSuccess.bind(this); - this.uploadError = this.uploadError.bind(this); - this.eventHandler = this.eventHandler.bind(this); + function uploadSuccess(url: string, source: FileUploadSource) { + if (source === 'paste') { + setMessage(url); + } else { + onSubmit([{ url }]); + } } - toggleCode() { - this.setState({ - inCodeMode: !this.state.inCodeMode - }); + function toggleCode() { + setInCodeMode(!inCodeMode); } - async submit(text) { - const { props, state } = this; - const { onSubmit } = this.props; - this.setState({ - inCodeMode: false - }); - props.deleteMessage(); - if(state.inCodeMode) { + async function submit() { + const text = chatEditor.current?.getValue() || ''; + + if (text === '') { + return; + } + + if (inCodeMode) { const output = await airlock.thread(evalCord(text)); onSubmit([{ code: { output, expression: text } }]); } else { onSubmit(tokenizeMessage(text)); } - this.chatEditor.current.editor.focus(); - this.setState({ currentInput: '' }); + + setInCodeMode(false); + setMessage(''); + chatEditor.current.focus(); } - uploadSuccess(url: string) { - const { props } = this; - if (this.state.uploadingPaste) { - this.chatEditor.current.editor.setValue(url); - this.setState({ uploadingPaste: false }); - } else { - props.onSubmit([{ url }]); - } - } - - uploadError(error) { - // no-op for now - } - - onPaste(codemirrorInstance, event: ClipboardEvent) { - if (!event.clipboardData || !event.clipboardData.files.length) { - return; - } - this.setState({ uploadingPaste: true }); - event.preventDefault(); - event.stopPropagation(); - this.uploadFiles(event.clipboardData.files); - } - - uploadFiles(files: FileList | File[]) { - if (!this.props.canUpload) { - return; - } - Array.from(files).forEach((file) => { - this.props - .uploadDefault(file) - .then(this.uploadSuccess) - .catch(this.uploadError); - }); - } - - eventHandler(value) { - this.setState({ currentInput: value }); - } - - render() { - const { props, state } = this; - - const color = props.ourContact ? uxToHex(props.ourContact.color) : '000000'; - - const sigilClass = props.ourContact ? '' : 'mix-blend-diff'; - - const avatar = - props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? ( - - ) : ( - - - - ); - - return ( - - - {avatar} - - - - - - {this.props.canUpload ? ( - - {this.props.uploading ? ( - - ) : ( - - this.props.promptUpload().then(this.uploadSuccess) - } - /> - )} - - ) : null} - {MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? - this.chatEditor.current.submit()} - > - - - : null} + return ( + + + - ); - } -} + onPaste(e)} + placeholder={placeholder} + /> + + + + {canUpload && ( + + {uploading ? ( + + ) : ( + + promptUpload().then(url => uploadSuccess(url, 'direct')) + } + /> + )} + + )} + {MOBILE_BROWSER_REGEX.test(navigator.userAgent) && ( + + )} + + ); +}); // @ts-ignore withLocalState prop passing weirdness export default withLocalState, 'hideAvatars', ChatInput>( - // @ts-ignore withLocalState prop passing weirdness - withStorage(ChatInput, { accept: 'image/*' }), + ChatInput, ['hideAvatars'] ); diff --git a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx index bd2f340907..9233fa2a47 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx @@ -1,18 +1,50 @@ import { Col } from '@tlon/indigo-react'; import { Content, Graph, Post } from '@urbit/api'; import bigInt, { BigInteger } from 'big-integer'; -import _ from 'lodash'; -import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; -import { useFileDrag } from '~/logic/lib/useDrag'; -import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import create from 'zustand'; +import { persist } from 'zustand/middleware'; +import { useFileUpload } from '~/logic/lib/useFileUpload'; import { useOurContact } from '~/logic/state/contact'; import { useGraphTimesent } from '~/logic/state/graph'; import ShareProfile from '~/views/apps/chat/components/ShareProfile'; import { Loading } from '~/views/components/Loading'; import SubmitDragger from '~/views/components/SubmitDragger'; -import ChatInput, { ChatInput as NakedChatInput } from './ChatInput'; +import ChatInput from './ChatInput'; import ChatWindow from './ChatWindow'; +interface useChatStoreType { + id: string; + message: string; + messageStore: Record; + restore: (id: string) => void; + setMessage: (message: string) => void; +} + +const unsentKey = 'chat-unsent'; +export const useChatStore = create(persist((set, get) => ({ + id: '', + message: '', + messageStore: {}, + restore: (id: string) => { + const store = get().messageStore; + set({ + id, + messageStore: store, + message: store[id] || '' + }); + }, + setMessage: (message: string) => { + const store = get().messageStore; + store[get().id] = message; + + set({ message, messageStore: store }); + } +}), { + name: unsentKey, + whitelist: ['messageStore'] +})); + interface ChatPaneProps { /** * A key to uniquely identify a ChatPane instance. Should be either the @@ -79,37 +111,13 @@ export function ChatPane(props: ChatPaneProps): ReactElement { } = props; const graphTimesentMap = useGraphTimesent(id); const ourContact = useOurContact(); - const chatInput = useRef(); + const { restore, setMessage } = useChatStore(s => ({ setMessage: s.setMessage, restore: s.restore })); + const { canUpload, drag } = useFileUpload({ + onSuccess: url => onSubmit([{ url }]) + }); - const onFileDrag = useCallback( - (files: FileList | File[]) => { - if (!chatInput.current) { - return; - } - (chatInput.current as NakedChatInput)?.uploadFiles(files); - }, - [chatInput] - ); - - const { bind, dragging } = useFileDrag(onFileDrag); - - const [unsent, setUnsent] = useLocalStorageState>( - 'chat-unsent', - {} - ); - - const appendUnsent = useCallback( - (u: string) => setUnsent(s => ({ ...s, [id]: u })), - [id] - ); - - const clearUnsent = useCallback(() => { - setUnsent((s) => { - if (id in s) { - return _.omit(s, id); - } - return s; - }); + useEffect(() => { + restore(id); }, [id]); const scrollTo = new URLSearchParams(location.search).get('msg'); @@ -123,7 +131,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement { const onReply = useCallback( (msg: Post) => { const message = props.onReply(msg); - setUnsent(s => ({ ...s, [id]: message })); + setMessage(message); }, [id, props.onReply] ); @@ -134,13 +142,13 @@ export function ChatPane(props: ChatPaneProps): ReactElement { return ( // @ts-ignore bind typings - + setShowBanner(false)} /> - {dragging && } + {canUpload && drag.dragging && } {canWrite && ( )} diff --git a/pkg/interface/src/views/apps/links/components/LinkBlockInput.tsx b/pkg/interface/src/views/apps/links/components/LinkBlockInput.tsx index 732031128b..ad49830e0f 100644 --- a/pkg/interface/src/views/apps/links/components/LinkBlockInput.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkBlockInput.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useState } from 'react'; import { Box, LoadingSpinner, Action, Row } from '@tlon/indigo-react'; -import useStorage from '~/logic/lib/useStorage'; import { StatelessUrlInput } from '~/views/components/StatelessUrlInput'; +import SubmitDragger from '~/views/components/SubmitDragger'; import { Association, resourceFromPath, createPost } from '@urbit/api'; import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks'; import useGraphState, { GraphState } from '~/logic/state/graph'; +import { useFileUpload } from '~/logic/lib/useFileUpload'; interface LinkBlockInputProps { size: string; @@ -22,7 +23,6 @@ export function LinkBlockInput(props: LinkBlockInputProps) { const [focussed, setFocussed] = useState(false); const addPost = useGraphState(selGraph); - const { uploading, canUpload, promptUpload } = useStorage(); const onFocus = useCallback(() => { setFocussed(true); @@ -41,6 +41,10 @@ export function LinkBlockInput(props: LinkBlockInputProps) { setValid(URLparser.test(val) || Boolean(parsePermalink(val))); }, []); + const { uploading, canUpload, promptUpload, drag } = useFileUpload({ + onSuccess: handleChange + }); + const doPost = () => { const text = ''; const { ship, name } = resourceFromPath(association.resource); @@ -83,7 +87,9 @@ export function LinkBlockInput(props: LinkBlockInputProps) { p="2" position="relative" backgroundColor="washedGray" + {...drag.bind} > + {drag.dragging && } {uploading ? ( { - const { canUpload, uploadDefault, uploading, promptUpload } = - useStorage(); const addPost = useGraphState(s => s.addPost); const [submitFocused, setSubmitFocused] = useState(false); @@ -26,6 +23,17 @@ const LinkSubmit = (props: LinkSubmitProps) => { const [disabled, setDisabled] = useState(false); const [linkValid, setLinkValid] = useState(false); + const { + canUpload, + uploading, + promptUpload, + drag, + onPaste + } = useFileUpload({ + onSuccess: setLinkValue, + multiple: false + }); + const doPost = () => { const url = linkValue; const text = linkTitle ? linkTitle : linkValue; @@ -98,18 +106,6 @@ const LinkSubmit = (props: LinkSubmitProps) => { setLinkValid(validateLink(linkValue)); }, [linkValue]); - const onFileDrag = useCallback( - (files: FileList | File[], e: DragEvent): void => { - if (!canUpload) { - return; - } - uploadDefault(files[0]).then(setLinkValue); - }, - [uploadDefault, canUpload] - ); - - const { bind, dragging } = useFileDrag(onFileDrag); - const onLinkChange = () => { const link = validateLink(linkValue); setLinkValid(link); @@ -117,17 +113,6 @@ const LinkSubmit = (props: LinkSubmitProps) => { useEffect(onLinkChange, [linkValue]); - const onPaste = useCallback( - (event: ClipboardEvent) => { - if (!event.clipboardData || !event.clipboardData.files.length) { - return; - } - event.preventDefault(); - event.stopPropagation(); - uploadDefault(event.clipboardData.files[0]).then(setLinkValue); - }, [setLinkValue, uploadDefault] - ); - const onKeyPress = (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -145,7 +130,7 @@ const LinkSubmit = (props: LinkSubmitProps) => { borderColor={submitFocused ? 'black' : 'lightGray'} width='100%' borderRadius={2} - {...bind} + {...drag.bind} > {uploading && { > } - {dragging && } + {drag.dragging && } { + const onFileUpload = useCallback( + async (files: FileList | File[], { canUpload, uploadDefault }: IuseStorage) => { if (!canUpload || !editor.current) { return; } @@ -79,10 +77,14 @@ export function MarkdownEditor( doc.setValue(doc.getValue().replace(placeholder, markdown)); }); }, - [uploadDefault, canUpload, value, onChange] + [value, onChange] ); - const { bind, dragging } = useFileDrag(onFileDrag); + const { + drag: { bind, dragging } + } = useFileUpload({ + onFiles: onFileUpload + }); return ( ) => { airlock.poke(addBucket(values.newBucket)); actions.resetForm({ values: { newBucket: '' } }); + setAdding(false); }, - [] + [setAdding] ); const onSelect = useCallback( @@ -52,7 +57,7 @@ export function BucketList({ ); return ( - +
setAdding(false)}> Cancel - + {!adding && + + } + {adding && + + }