From 1eb6c47c83975e2d1a99b0a7a15e21c238f26388 Mon Sep 17 00:00:00 2001 From: Tyler Brown Cifu Shuster Date: Thu, 26 Nov 2020 09:32:21 -0800 Subject: [PATCH 1/4] s3: improves drag-and-drop behavior Fixes #3992 ...to the extent possible. Adds handlers for better releasing drag state --- pkg/interface/src/logic/lib/useDrag.ts | 57 ++++++++++++++++--- pkg/interface/src/logic/lib/util.ts | 2 +- .../src/views/apps/chat/ChatResource.tsx | 2 +- .../views/apps/chat/components/ChatInput.tsx | 14 ++--- .../src/views/components/s3-upload.tsx | 16 ++++-- 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/pkg/interface/src/logic/lib/useDrag.ts b/pkg/interface/src/logic/lib/useDrag.ts index 5b4878fb2..43072deca 100644 --- a/pkg/interface/src/logic/lib/useDrag.ts +++ b/pkg/interface/src/logic/lib/useDrag.ts @@ -1,15 +1,39 @@ import { useState, useCallback, useMemo, useEffect } from "react"; -function validateDragEvent(e: DragEvent): FileList | null { - const files = e.dataTransfer?.files; - console.log(files); - if(!files?.length) { - return null; +function validateDragEvent(e: DragEvent): FileList | File[] | true | null { + const files: File[] = []; + let valid = false; + if (e.dataTransfer?.files) { + Array.from(e.dataTransfer.files).forEach(f => files.push(f)); } - return files || null; + if (e.dataTransfer?.items) { + Array.from(e.dataTransfer.items || []) + .filter((i) => i.kind === 'file') + .forEach(f => { + valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security + const data = f.getAsFile(); + if (data) { + files.push(data); + } + }); + } + if (files.length) { + return [...new Set(files)]; + } + if (navigator.userAgent.includes('Safari')) { + if (e.dataTransfer?.effectAllowed === 'all') { + valid = true; + } else if (e.dataTransfer?.files.length) { + return e.dataTransfer.files; + } + } + if (valid) { + return true; + } + return null; } -export function useFileDrag(dragged: (f: FileList) => void) { +export function useFileDrag(dragged: (f: FileList | File[]) => void) { const [dragging, setDragging] = useState(false); const onDragEnter = useCallback( @@ -25,11 +49,11 @@ export function useFileDrag(dragged: (f: FileList) => void) { const onDrop = useCallback( (e: DragEvent) => { setDragging(false); - e.preventDefault(); const files = validateDragEvent(e); - if (!files) { + if (!files || files === true) { return; } + e.preventDefault(); dragged(files); }, [setDragging, dragged] @@ -37,6 +61,9 @@ export function useFileDrag(dragged: (f: FileList) => void) { const onDragOver = useCallback( (e: DragEvent) => { + if (!validateDragEvent(e)) { + return; + } e.preventDefault(); setDragging(true); }, @@ -53,6 +80,18 @@ export function useFileDrag(dragged: (f: FileList) => void) { [setDragging] ); + useEffect(() => { + const mouseleave = (e) => { + if (!e.relatedTarget && !e.toElement) { + setDragging(false); + } + }; + document.body.addEventListener('mouseout', mouseleave); + return () => { + document.body.removeEventListener('mouseout', mouseleave); + } + }, [setDragging]); + const bind = { onDragLeave, onDragOver, diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 1ebe6e48b..dc6cb6338 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -353,4 +353,4 @@ export function usePreventWindowUnload(shouldPreventDefault: boolean, message = export function pluralize(text: string, isPlural = false, vowel = false) { return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`; -} +} \ No newline at end of file diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 0d8c905f7..65171325b 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -68,7 +68,7 @@ export function ChatResource(props: ChatResourceProps) { const chatInput = useRef(); const onFileDrag = useCallback( - (files: FileList) => { + (files: FileList | File[]) => { if (!chatInput.current) { return; } diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 09ff23904..fd6773a17 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import ChatEditor from './chat-editor'; import { S3Upload } from '~/views/components/s3-upload' ; -import { uxToHex } from '~/logic/lib/util'; +import { fileListFromFileArray, uxToHex } from '~/logic/lib/util'; import { Sigil } from '~/logic/lib/sigil'; import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; import GlobalApi from '~/logic/api/global'; @@ -150,16 +150,14 @@ export default class ChatInput extends Component this.uploadFiles(event.clipboardData.files); } - uploadFiles(files: FileList) { + uploadFiles(files: FileList | File[]) { if (!this.readyToUpload()) { return; } - if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current) -return; - this.s3Uploader.current.inputRef.current.files = files; - const fire = document.createEvent('HTMLEvents'); - fire.initEvent('change', true, true); - this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire); + if (!this.s3Uploader.current) { + return; + } + this.s3Uploader.current.uploadFiles(files); } render() { diff --git a/pkg/interface/src/views/components/s3-upload.tsx b/pkg/interface/src/views/components/s3-upload.tsx index d89ef154a..89b7c0627 100644 --- a/pkg/interface/src/views/components/s3-upload.tsx +++ b/pkg/interface/src/views/components/s3-upload.tsx @@ -51,6 +51,7 @@ export class S3Upload extends Component { this.s3 = new S3Client(); this.setCredentials(props.credentials, props.configuration); this.inputRef = React.createRef(); + this.uploadFiles = this.uploadFiles.bind(this); } isReady(creds, config): boolean { @@ -84,13 +85,9 @@ export class S3Upload extends Component { ); } - onChange(): void { + uploadFiles(files: FileList | File[]) { const { props } = this; - if (!this.inputRef.current) { return; } - let files = this.inputRef.current.files; - if (!files || files.length <= 0) { return; } - - let file = files.item(0); + let file = ('item' in files) ? files.item(0) : files[0]; if (!file) { return; } const fileParts = file.name.split('.'); const fileName = fileParts.slice(0, -1); @@ -118,6 +115,13 @@ export class S3Upload extends Component { }, 200); } + onChange(): void { + if (!this.inputRef.current) { return; } + let files = this.inputRef.current.files; + if (!files || files.length <= 0) { return; } + this.uploadFiles(files); + } + onClick() { if (!this.inputRef.current) { return; } this.inputRef.current.click(); From f71cd55486df626924e3d3309b464b6c85eb4899 Mon Sep 17 00:00:00 2001 From: Tyler Brown Cifu Shuster Date: Mon, 30 Nov 2020 11:02:34 -0800 Subject: [PATCH 2/4] publish: allow drag-and-drop uploading of images Fixes https://github.com/urbit/landscape/issues/183 --- pkg/interface/src/logic/lib/useDrag.ts | 4 +- .../views/apps/publish/PublishResource.tsx | 1 + .../apps/publish/components/EditPost.tsx | 6 ++- .../publish/components/MarkdownEditor.tsx | 41 ++++++++++++++++++- .../apps/publish/components/MarkdownField.tsx | 3 +- .../apps/publish/components/NoteForm.tsx | 14 ++++--- .../apps/publish/components/NoteRoutes.tsx | 3 +- .../publish/components/NotebookRoutes.tsx | 6 ++- .../apps/publish/components/new-post.tsx | 4 +- 9 files changed, 66 insertions(+), 16 deletions(-) diff --git a/pkg/interface/src/logic/lib/useDrag.ts b/pkg/interface/src/logic/lib/useDrag.ts index 43072deca..2ec374d48 100644 --- a/pkg/interface/src/logic/lib/useDrag.ts +++ b/pkg/interface/src/logic/lib/useDrag.ts @@ -33,7 +33,7 @@ function validateDragEvent(e: DragEvent): FileList | File[] | true | null { return null; } -export function useFileDrag(dragged: (f: FileList | File[]) => void) { +export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => void) { const [dragging, setDragging] = useState(false); const onDragEnter = useCallback( @@ -54,7 +54,7 @@ export function useFileDrag(dragged: (f: FileList | File[]) => void) { return; } e.preventDefault(); - dragged(files); + dragged(files, e); }, [setDragging, dragged] ); diff --git a/pkg/interface/src/views/apps/publish/PublishResource.tsx b/pkg/interface/src/views/apps/publish/PublishResource.tsx index 2524babe8..9719a6314 100644 --- a/pkg/interface/src/views/apps/publish/PublishResource.tsx +++ b/pkg/interface/src/views/apps/publish/PublishResource.tsx @@ -39,6 +39,7 @@ export function PublishResource(props: PublishResourceProps) { hideNicknames={props.hideNicknames} remoteContentPolicy={props.remoteContentPolicy} graphs={props.graphs} + s3={props.s3} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/EditPost.tsx b/pkg/interface/src/views/apps/publish/components/EditPost.tsx index 8e96ec82b..0f6ed4dcc 100644 --- a/pkg/interface/src/views/apps/publish/components/EditPost.tsx +++ b/pkg/interface/src/views/apps/publish/components/EditPost.tsx @@ -4,7 +4,7 @@ import { PostFormSchema, PostForm } from "./NoteForm"; import { FormikHelpers } from "formik"; import GlobalApi from "~/logic/api/global"; import { RouteComponentProps, useLocation } from "react-router-dom"; -import { GraphNode, TextContent, Association } from "~/types"; +import { GraphNode, TextContent, Association, S3State } from "~/types"; import { getLatestRevision, editPost } from "~/logic/lib/publish"; import {useWaitForProps} from "~/logic/lib/useWaitForProps"; interface EditPostProps { @@ -13,10 +13,11 @@ interface EditPostProps { note: GraphNode; api: GlobalApi; book: string; + s3: S3State; } export function EditPost(props: EditPostProps & RouteComponentProps) { - const { note, book, noteId, api, ship, history } = props; + const { note, book, noteId, api, ship, history, s3 } = props; const [revNum, title, body] = getLatestRevision(note); const location = useLocation(); @@ -53,6 +54,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) { cancel history={history} onSubmit={onSubmit} + s3={s3} submitLabel="Update" loadingText="Updating..." /> diff --git a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx index 9fdef30c1..90f2d5e91 100644 --- a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx +++ b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx @@ -1,7 +1,8 @@ -import React, { useCallback } from "react"; -import { UnControlled as CodeEditor } from "react-codemirror2"; +import React, { createRef, useCallback, useRef } from "react"; +import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2"; import { useFormikContext } from 'formik'; import { Prompt } from 'react-router-dom'; +import { Editor } from 'codemirror'; import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util"; import { PropFunc } from "~/types/util"; @@ -12,6 +13,10 @@ import "codemirror/addon/display/placeholder"; import "codemirror/lib/codemirror.css"; import { Box } from "@tlon/indigo-react"; +import { useFileDrag } from "~/logic/lib/useDrag"; +import { S3Upload, SubmitDragger } from "~/views/components/s3-upload"; +import { useS3 } from "~/logic/lib/useS3"; +import { S3State } from "~/types"; const MARKDOWN_CONFIG = { name: "markdown", @@ -22,6 +27,7 @@ interface MarkdownEditorProps { value: string; onChange: (s: string) => void; onBlur?: (e: any) => void; + s3: S3State; } const PromptIfDirty = () => { @@ -50,6 +56,8 @@ export function MarkdownEditor( placeholder: placeholder || "", }; + const editor: React.RefObject = useRef(); + const handleChange = useCallback( (_e, _d, v: string) => { onChange(v); @@ -64,6 +72,29 @@ export function MarkdownEditor( [onBlur] ); + const { uploadDefault, canUpload } = useS3(props.s3); + + const onFileDrag = useCallback( + async (files: FileList | File[], e: DragEvent) => { + if (!canUpload || !editor.current) { + return; + } + const codeMirror: Editor = editor.current.editor; + const doc = codeMirror.getDoc(); + + Array.from(files).forEach(async file => { + const placeholder = `![Uploading ${file.name}](...)`; + doc.setValue(doc.getValue() + placeholder); + const url = await uploadDefault(file); + const markdown = `![${file.name}](${url})`; + doc.setValue(doc.getValue().replace(placeholder, markdown)); + }); + }, + [uploadDefault, canUpload, value, onChange] + ); + + const { bind, dragging } = useFileDrag(onFileDrag); + return ( bind.onDragLeave(e)} + onDragOver={(editor, e) => bind.onDragOver(e)} + onDrop={(editor, e) => bind.onDrop(e)} + onDragEnter={(editor, e) => bind.onDragEnter(e)} /> + {dragging && } ); } diff --git a/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx b/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx index 2ce4f236b..750a64978 100644 --- a/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx +++ b/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx @@ -6,6 +6,7 @@ import { MarkdownEditor } from "./MarkdownEditor"; export const MarkdownField = ({ id, + s3, ...rest }: { id: string } & Parameters[0]) => { const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id); @@ -13,7 +14,6 @@ export const MarkdownField = ({ const handleBlur = useCallback( (e: any) => { _.set(e, "target.id", id); - console.log(e); onBlur && onBlur(e); }, [onBlur, id] @@ -35,6 +35,7 @@ export const MarkdownField = ({ onBlur={handleBlur} value={value} onChange={setValue} + s3={s3} /> {error} diff --git a/pkg/interface/src/views/apps/publish/components/NoteForm.tsx b/pkg/interface/src/views/apps/publish/components/NoteForm.tsx index 8333b9d69..6fbaaee20 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteForm.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteForm.tsx @@ -5,10 +5,11 @@ import { Row, Col, Button -} from '@tlon/indigo-react'; -import { AsyncButton } from '../../../components/AsyncButton'; -import { Formik, Form, FormikHelpers } from 'formik'; -import { MarkdownField } from './MarkdownField'; +} from "@tlon/indigo-react"; +import { AsyncButton } from "../../../components/AsyncButton"; +import { Formik, Form, FormikHelpers } from "formik"; +import { MarkdownField } from "./MarkdownField"; +import { S3State } from "~/types"; interface PostFormProps { initial: PostFormSchema; @@ -20,6 +21,7 @@ interface PostFormProps { ) => Promise; submitLabel: string; loadingText: string; + s3: S3State; } const formSchema = Yup.object({ @@ -33,7 +35,7 @@ export interface PostFormSchema { } export function PostForm(props: PostFormProps) { - const { initial, onSubmit, cancel, submitLabel, loadingText, history } = props; + const { initial, onSubmit, submitLabel, loadingText, s3, cancel, history } = props; return ( @@ -64,7 +66,7 @@ export function PostForm(props: PostFormProps) { type="button">Cancel} - + diff --git a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx index 843202e1f..52d00018a 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx @@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom"; import Note from "./Note"; import { EditPost } from "./EditPost"; -import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group } from "~/types"; +import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group, S3State } from "~/types"; interface NoteRoutesProps { ship: string; @@ -21,6 +21,7 @@ interface NoteRoutesProps { hideAvatars: boolean; baseUrl?: string; rootUrl?: string; + s3: S3State; } export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) { diff --git a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx index eefc9236a..61041c604 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx @@ -8,7 +8,8 @@ import { Groups, Contacts, Rolodex, - LocalUpdateRemoteContentPolicy + LocalUpdateRemoteContentPolicy, + S3State } from "~/types"; import { Center, LoadingSpinner } from "@tlon/indigo-react"; import { Notebook as INotebook } from "~/types/publish-update"; @@ -35,6 +36,7 @@ interface NotebookRoutesProps { association: Association; remoteContentPolicy: LocalUpdateRemoteContentPolicy; associations: Associations; + s3: S3State; } export function NotebookRoutes( @@ -79,6 +81,7 @@ export function NotebookRoutes( association={props.association} graph={graph} baseUrl={baseUrl} + s3={props.s3} /> )} /> @@ -110,6 +113,7 @@ export function NotebookRoutes( hideAvatars={props.hideAvatars} hideNicknames={props.hideNicknames} remoteContentPolicy={props.remoteContentPolicy} + s3={props.s3} {...routeProps} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/new-post.tsx b/pkg/interface/src/views/apps/publish/components/new-post.tsx index b269425f4..d44fa2885 100644 --- a/pkg/interface/src/views/apps/publish/components/new-post.tsx +++ b/pkg/interface/src/views/apps/publish/components/new-post.tsx @@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom"; import { PostForm, PostFormSchema } from "./NoteForm"; import {createPost} from "~/logic/api/graph"; import {Graph} from "~/types/graph-update"; -import {Association} from "~/types"; +import {Association, S3State} from "~/types"; import {newPost} from "~/logic/lib/publish"; interface NewPostProps { @@ -16,6 +16,7 @@ interface NewPostProps { graph: Graph; association: Association; baseUrl: string; + s3: S3State; } export default function NewPost(props: NewPostProps & RouteComponentProps) { @@ -50,6 +51,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) { onSubmit={onSubmit} submitLabel="Publish" loadingText="Posting..." + s3={props.s3} /> ); } From e5ea163bfac74bcb306f462167ad33bd476557aa Mon Sep 17 00:00:00 2001 From: Tyler Brown Cifu Shuster Date: Wed, 2 Dec 2020 06:59:14 -0800 Subject: [PATCH 3/4] s3: unifies upload component Fixes https://github.com/urbit/landscape/issues/191 --- pkg/interface/src/logic/lib/useS3.ts | 75 ++++- .../src/views/apps/chat/ChatResource.tsx | 41 +-- .../views/apps/chat/components/ChatInput.tsx | 72 ++-- .../src/views/apps/links/LinkResource.tsx | 2 +- .../apps/links/components/LinkSubmit.tsx | 230 +++++++++++++ .../apps/links/components/link-submit.tsx | 310 ------------------ .../publish/components/MarkdownEditor.tsx | 4 +- .../src/views/components/ImageInput.tsx | 10 +- .../src/views/components/SubmitDragger.tsx | 27 ++ .../src/views/components/s3-upload.tsx | 161 --------- pkg/interface/src/views/components/withS3.tsx | 12 + 11 files changed, 388 insertions(+), 556 deletions(-) create mode 100644 pkg/interface/src/views/apps/links/components/LinkSubmit.tsx delete mode 100644 pkg/interface/src/views/apps/links/components/link-submit.tsx create mode 100644 pkg/interface/src/views/components/SubmitDragger.tsx delete mode 100644 pkg/interface/src/views/components/s3-upload.tsx create mode 100644 pkg/interface/src/views/components/withS3.tsx diff --git a/pkg/interface/src/logic/lib/useS3.ts b/pkg/interface/src/logic/lib/useS3.ts index f040924dd..e6652d716 100644 --- a/pkg/interface/src/logic/lib/useS3.ts +++ b/pkg/interface/src/logic/lib/useS3.ts @@ -1,32 +1,36 @@ -import { useCallback, useMemo, useEffect, useRef } from "react"; +import { useCallback, useMemo, useEffect, useRef, useState } from "react"; import { S3State } from "../../types/s3-update"; import S3 from "aws-sdk/clients/s3"; -export function useS3(s3: S3State) { - const { configuration, credentials } = s3; +export interface IuseS3 { + canUpload: boolean; + upload: (file: File, bucket: string) => Promise; + uploadDefault: (file: File) => Promise; + uploading: boolean; + promptUpload: () => Promise; +} + +const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { + const [uploading, setUploading] = useState(false); const client = useRef(null); useEffect(() => { - if (!credentials) { + if (!s3.credentials) { return; } - client.current = new S3({ credentials, endpoint: credentials.endpoint }); - }, [credentials]); + client.current = new S3({ + credentials: s3.credentials, + endpoint: s3.credentials.endpoint + }); + }, [s3.credentials]); const canUpload = useMemo( () => - (client && credentials && configuration.currentBucket !== "") || false, - [credentials, configuration.currentBucket, client] + (client && s3.credentials && s3.configuration.currentBucket !== "") || false, + [s3.credentials, s3.configuration.currentBucket, client] ); - const uploadDefault = useCallback(async (file: File) => { - if (configuration.currentBucket === "") { - throw new Error("current bucket not set"); - } - return upload(file, configuration.currentBucket); - }, []); - const upload = useCallback( async (file: File, bucket: string) => { if (!client.current) { @@ -40,12 +44,47 @@ export function useS3(s3: S3State) { ACL: "public-read", ContentType: file.type, }; + + setUploading(true); const { Location } = await client.current.upload(params).promise(); + + setUploading(false); + return Location; }, - [client] + [client, setUploading] ); - return { canUpload, upload, uploadDefault }; -} + const uploadDefault = useCallback(async (file: File) => { + if (s3.configuration.currentBucket === "") { + throw new Error("current bucket not set"); + } + return upload(file, s3.configuration.currentBucket); + }, []); + + const promptUpload = useCallback( + () => { + return new Promise((resolve, reject) => { + const fileSelector = document.createElement('input'); + fileSelector.setAttribute('type', 'file'); + fileSelector.setAttribute('accept', accept); + fileSelector.addEventListener('change', () => { + const files = fileSelector.files; + if (!files || files.length <= 0) { + reject(); + return; + } + uploadDefault(files[0]).then(resolve); + }) + fileSelector.click(); + }) + + }, + [uploadDefault] + ); + + return { canUpload, upload, uploadDefault, uploading, promptUpload }; +}; + +export default useS3; \ No newline at end of file diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 65171325b..33d5038cb 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -9,8 +9,9 @@ import { useFileDrag } from '~/logic/lib/useDrag'; import ChatWindow from './components/ChatWindow'; import ChatInput from './components/ChatInput'; import GlobalApi from '~/logic/api/global'; -import { SubmitDragger } from '~/views/components/s3-upload'; +import SubmitDragger from '~/views/components/SubmitDragger'; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; +import useS3 from '~/logic/lib/useS3'; type ChatResourceProps = StoreState & { association: Association; @@ -65,20 +66,6 @@ export function ChatResource(props: ChatResourceProps) { const ourContact = contacts?.[window.ship]; const lastMsgNum = envelopes.length || 0; - const chatInput = useRef(); - - const onFileDrag = useCallback( - (files: FileList | File[]) => { - if (!chatInput.current) { - return; - } - chatInput.current?.uploadFiles(files); - }, - [chatInput?.current] - ); - - const { bind, dragging } = useFileDrag(onFileDrag); - const [unsent, setUnsent] = useLocalStorageState>( 'chat-unsent', {} @@ -89,13 +76,29 @@ export function ChatResource(props: ChatResourceProps) { [station] ); - const clearUnsent = useCallback(() => setUnsent(s => _.omit(s, station)), [ - station - ]); + const clearUnsent = useCallback( + () => setUnsent(s => _.omit(s, station)), + [station] + ); + + const chatInput = useRef(); + + const onFileDrag = useCallback( + (files: FileList | File[]) => { + if (!chatInput.current) { + return; + } + chatInput.current.uploadFiles(files); + }, + [chatInput.current] + ); + + const { bind, dragging } = useFileDrag(onFileDrag); const scrollTo = new URLSearchParams(location.search).get('msg'); + useEffect(() => { - const clear = () => { + const clear = () => { props.history.replace(location.pathname); }; setTimeout(clear, 10000); diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index fd6773a17..9bf3f4486 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -1,15 +1,16 @@ import React, { Component } from 'react'; import ChatEditor from './chat-editor'; -import { S3Upload } from '~/views/components/s3-upload' ; -import { fileListFromFileArray, uxToHex } from '~/logic/lib/util'; +import { IuseS3 } from '~/logic/lib/useS3'; +import { uxToHex } from '~/logic/lib/util'; import { Sigil } from '~/logic/lib/sigil'; import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; import GlobalApi from '~/logic/api/global'; import { Envelope } from '~/types/chat-update'; import { Contacts } from '~/types'; -import { Row, BaseImage, Box, Icon } from '@tlon/indigo-react'; +import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react'; +import withS3 from '~/views/components/withS3'; -interface ChatInputProps { +type ChatInputProps = IuseS3 & { api: GlobalApi; numMsgs: number; station: any; @@ -22,7 +23,6 @@ interface ChatInputProps { message: string; deleteMessage(): void; hideAvatars: boolean; - onPaste?(): void; } interface ChatInputState { @@ -31,8 +31,7 @@ interface ChatInputState { uploadingPaste: boolean; } -export default class ChatInput extends Component { - public s3Uploader: React.RefObject; +class ChatInput extends Component { private chatEditor: React.RefObject; constructor(props) { @@ -44,11 +43,12 @@ export default class ChatInput extends Component uploadingPaste: false }; - this.s3Uploader = React.createRef(); this.chatEditor = React.createRef(); this.submit = this.submit.bind(this); this.toggleCode = this.toggleCode.bind(this); + this.uploadSuccess = this.uploadSuccess.bind(this); + this.uploadError = this.uploadError.bind(this); } toggleCode() { @@ -136,10 +136,6 @@ export default class ChatInput extends Component // no-op for now } - readyToUpload(): boolean { - return Boolean(this.s3Uploader.current?.inputRef.current); - } - onPaste(codemirrorInstance, event: ClipboardEvent) { if (!event.clipboardData || !event.clipboardData.files.length) { return; @@ -151,13 +147,14 @@ export default class ChatInput extends Component } uploadFiles(files: FileList | File[]) { - if (!this.readyToUpload()) { + if (!this.props.canUpload) { return; } - if (!this.s3Uploader.current) { - return; - } - this.s3Uploader.current.uploadFiles(files); + Array.from(files).forEach(file => { + this.props.uploadDefault(file) + .then(this.uploadSuccess) + .catch(this.uploadError); + }); } render() { @@ -187,13 +184,13 @@ export default class ChatInput extends Component {avatar} @@ -208,29 +205,26 @@ export default class ChatInput extends Component placeholder='Message...' /> - - - + {this.props.canUpload + ? this.props.uploading + ? + : this.props.promptUpload().then(this.uploadSuccess)} + /> + : null + } ); } } + +export default withS3(ChatInput, {accept: 'image/*'}); \ No newline at end of file diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index e01e14eef..546426805 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -9,7 +9,7 @@ import { uxToHex } from '~/logic/lib/util'; import { RouteComponentProps } from "react-router-dom"; import { LinkItem } from "./components/LinkItem"; -import { LinkSubmit } from "./components/link-submit"; +import LinkSubmit from "./components/LinkSubmit"; import { LinkPreview } from "./components/link-preview"; import { Comments } from "~/views/components/comments"; diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx new file mode 100644 index 000000000..601663584 --- /dev/null +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -0,0 +1,230 @@ +import { BaseInput, Box, Button, LoadingSpinner, Text } from "@tlon/indigo-react"; +import React, { useCallback, useState } from "react"; +import GlobalApi from "~/logic/api/global"; +import { useFileDrag } from "~/logic/lib/useDrag"; +import useS3 from "~/logic/lib/useS3"; +import { S3State } from "~/types"; +import SubmitDragger from "~/views/components/SubmitDragger"; +import { createPost } from "~/logic/api/graph"; +import { hasProvider } from "oembed-parser"; + +interface LinkSubmitProps { + api: GlobalApi; + s3: S3State; + name: string; + ship: string; +}; + +const LinkSubmit = (props: LinkSubmitProps) => { + let { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3); + + const [submitFocused, setSubmitFocused] = useState(false); + const [urlFocused, setUrlFocused] = useState(false); + const [linkValue, setLinkValueHook] = useState(''); + const [linkTitle, setLinkTitle] = useState(''); + const [disabled, setDisabled] = useState(false); + const [linkValid, setLinkValid] = useState(false); + + const doPost = () => { + const url = linkValue; + const text = linkTitle ? linkTitle : linkValue; + setDisabled(true); + const parentIndex = props.parentIndex || ''; + const post = createPost([ + { text }, + { url } + ], parentIndex); + + props.api.graph.addPost( + `~${props.ship}`, + props.name, + post + ).then(() => { + setDisabled(false); + setLinkValue(''); + setLinkTitle(''); + setLinkValid(false); + }); + }; + + const validateLink = (link) => { + const URLparser = new RegExp( + /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ + ); + + let linkValid = URLparser.test(link); + + if (!linkValid) { + linkValid = URLparser.test(`http://${link}`); + if (linkValid) { + link = `http://${link}`; + setLinkValue(link); + } + } + + if (linkValid) { + if (hasProvider(linkValue)) { + fetch(`https://noembed.com/embed?url=${linkValue}`) + .then(response => response.json()) + .then((result) => { + if (result.title && !linkTitle) { + setLinkTitle(result.title); + } + }).catch((error) => { /* noop*/ }); + } else if (!linkTitle) { + setLinkTitle(decodeURIComponent(link + .split('/') + .pop() + .split('.') + .slice(0, -1) + .join('.') + .replace('_', ' ') + .replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '') + )); + } + } + return link; + }; + + const onFileDrag = useCallback( + async (files: FileList | File[], e: DragEvent) => { + if (!canUpload) { + return; + } + uploadDefault(files[0]).then(setLinkValue); + }, + [uploadDefault, canUpload] + ); + + const { bind, dragging } = useFileDrag(onFileDrag); + + const onLinkChange = (linkValue: string) => { + setLinkValueHook(linkValue); + const link = validateLink(linkValue) + setLinkValid(link); + }; + + const setLinkValue = (linkValue: string) => { + onLinkChange(linkValue); + setLinkValueHook(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] + ); + + const onKeyPress = useCallback( + (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + doPost(); + } + }, [] + ); + + const placeholder = {canUpload + ? <> + Drop or{' '} + promptUpload().then(setLinkValue)} + >upload + {' '}a file, or paste a link here + + : 'Paste a link here' + }; + + return ( + <> + + {uploading && + + } + {dragging && } + + {!(linkValue || urlFocused || disabled) && placeholder} + onLinkChange(e.target.value)} + onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]} + onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]} + spellCheck="false" + onPaste={onPaste} + onKeyPress={onKeyPress} + value={linkValue} + /> + + setLinkTitle(e.target.value)} + onBlur={() => setSubmitFocused(false)} + onFocus={() => setSubmitFocused(true)} + spellCheck="false" + onKeyPress={onKeyPress} + value={linkTitle} + /> + + + + + + ); +}; + +export default LinkSubmit; \ No newline at end of file diff --git a/pkg/interface/src/views/apps/links/components/link-submit.tsx b/pkg/interface/src/views/apps/links/components/link-submit.tsx deleted file mode 100644 index ecd84b596..000000000 --- a/pkg/interface/src/views/apps/links/components/link-submit.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React, { Component } from 'react'; -import { hasProvider } from 'oembed-parser'; - -import { S3Upload, SubmitDragger } from '~/views/components/s3-upload'; -import { Box, Text, BaseInput, Button } from '@tlon/indigo-react'; -import GlobalApi from '~/logic/api/global'; -import { S3State } from '~/types'; - -import { createPost } from '~/logic/api/graph'; - -interface LinkSubmitProps { - api: GlobalApi; - s3: S3State; - name: string; - ship: string; -} - -interface LinkSubmitState { - linkValue: string; - linkTitle: string; - linkValid: boolean; - submitFocus: boolean; - urlFocus: boolean; - disabled: boolean; - dragover: boolean; -} - -export class LinkSubmit extends Component { - private s3Uploader: React.RefObject; - - constructor(props) { - super(props); - this.state = { - linkValue: '', - linkTitle: '', - linkValid: false, - submitFocus: false, - urlFocus: false, - disabled: false, - dragover: false - }; - this.setLinkValue = this.setLinkValue.bind(this); - this.setLinkTitle = this.setLinkTitle.bind(this); - this.onDragEnter = this.onDragEnter.bind(this); - this.onDrop = this.onDrop.bind(this); - this.onPaste = this.onPaste.bind(this); - this.uploadFiles = this.uploadFiles.bind(this); - this.s3Uploader = React.createRef(); - } - - onClickPost() { - const link = this.state.linkValue; - const title = this.state.linkTitle - ? this.state.linkTitle - : this.state.linkValue; - this.setState({ disabled: true }); - - const parentIndex = this.props.parentIndex || ''; - const post = createPost([ - { text: title }, - { url: link } - ], parentIndex); - - this.props.api.graph.addPost( - `~${this.props.ship}`, - this.props.name, - post - ).then((r) => { - this.setState({ - disabled: false, - linkValue: '', - linkTitle: '', - linkValid: false - }); - }); - } - - setLinkValid(linkValue) { - const URLparser = new RegExp( - /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ - ); - - let linkValid = URLparser.test(linkValue); - - if (!linkValid) { - linkValid = URLparser.test(`http://${linkValue}`); - if (linkValid) { - linkValue = `http://${linkValue}`; - } - } - - this.setState({ linkValid, linkValue }); - - if (linkValid) { - if (hasProvider(linkValue)) { - fetch(`https://noembed.com/embed?url=${linkValue}`) - .then(response => response.json()) - .then((result) => { - if (result.title && !this.state.linkTitle) { - this.setState({ linkTitle: result.title }); - } - }).catch((error) => { /* noop*/ }); - } else if (!this.state.linkTitle) { - this.setState({ - linkTitle: decodeURIComponent(linkValue - .split('/') - .pop() - .split('.') - .slice(0, -1) - .join('.') - .replace('_', ' ') - .replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '') - ) - }); - } - } - } - - setLinkValue(event) { - this.setState({ linkValue: event.target.value }); - this.setLinkValid(event.target.value); - } - - setLinkTitle(event) { - this.setState({ linkTitle: event.target.value }); - } - - uploadSuccess(url) { - this.setState({ linkValue: url }); - this.setLinkValid(url); - } - - uploadError(error) { - // no-op for now - } - - readyToUpload(): boolean { - return Boolean(this.s3Uploader.current && this.s3Uploader.current.inputRef.current); - } - - onDragEnter() { - if (!this.readyToUpload()) { - return; - } - this.setState({ dragover: true }); - } - - onDrop(event: DragEvent) { - this.setState({ dragover: false }); - if (!event.dataTransfer || !event.dataTransfer.files.length) { - return; - } - event.preventDefault(); - this.uploadFiles(event.dataTransfer.files); - } - - onPaste(event: ClipboardEvent) { - if (!event.clipboardData || !event.clipboardData.files.length) { - return; - } - event.preventDefault(); - event.stopPropagation(); - this.uploadFiles(event.clipboardData.files); - } - - uploadFiles(files: FileList) { - if (!this.readyToUpload()) { - return; - } - this.s3Uploader.current.inputRef.current.files = files; - const fire = document.createEvent('HTMLEvents'); - fire.initEvent('change', true, true); - this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire); - } - - render() { - const isS3Ready = - ( this.props.s3.credentials.secretAccessKey && - this.props.s3.credentials.endpoint && - this.props.s3.credentials.accessKeyId - ); - - return ( - <> - { - e.preventDefault(); - if (isS3Ready) { - this.setState({ dragover: true }); - } - }} - onDragLeave={() => this.setState({ dragover: false })} - onDrop={this.onDrop} - > - {this.state.dragover ? : null} - - { - ( this.state.linkValue || - this.state.urlFocus || - this.state.disabled - ) ? null : ( - isS3Ready ? ( - - Drop or - { - if (!this.readyToUpload()) { - return; - } - this.s3Uploader.current.inputRef.current.click(); - }} - > upload - a file, or paste a link here - - ) : ( - - Paste a link here - - ) - ) - } - {!this.state.disabled && isS3Ready ? : null} - this.setState({ submitFocus: false, urlFocus: false })} - onFocus={() => this.setState({ submitFocus: true, urlFocus: true })} - spellCheck="false" - onPaste={this.onPaste} - onKeyPress={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.onClickPost(); - } - }} - value={this.state.linkValue} - /> - - this.setState({ submitFocus: false })} - onFocus={() => this.setState({ submitFocus: true })} - spellCheck="false" - onKeyPress={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.onClickPost(); - } - }} - value={this.state.linkTitle} - /> - - - - - - ) ; - } -} - -export default LinkSubmit; diff --git a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx index 90f2d5e91..1e1cce85d 100644 --- a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx +++ b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx @@ -14,8 +14,8 @@ import "codemirror/addon/display/placeholder"; import "codemirror/lib/codemirror.css"; import { Box } from "@tlon/indigo-react"; import { useFileDrag } from "~/logic/lib/useDrag"; -import { S3Upload, SubmitDragger } from "~/views/components/s3-upload"; -import { useS3 } from "~/logic/lib/useS3"; +import SubmitDragger from "~/views/components/SubmitDragger"; +import useS3 from "~/logic/lib/useS3"; import { S3State } from "~/types"; const MARKDOWN_CONFIG = { diff --git a/pkg/interface/src/views/components/ImageInput.tsx b/pkg/interface/src/views/components/ImageInput.tsx index e4f7870b5..5583b2d43 100644 --- a/pkg/interface/src/views/components/ImageInput.tsx +++ b/pkg/interface/src/views/components/ImageInput.tsx @@ -11,7 +11,7 @@ import { } from "@tlon/indigo-react"; import { useField } from "formik"; import { S3State } from "~/types/s3-update"; -import { useS3 } from "~/logic/lib/useS3"; +import useS3 from "~/logic/lib/useS3"; type ImageInputProps = Parameters[0] & { id: string; @@ -23,9 +23,7 @@ type ImageInputProps = Parameters[0] & { export function ImageInput(props: ImageInputProps) { const { id, label, s3, caption, placeholder, ...rest } = props; - const { uploadDefault, canUpload } = useS3(s3); - - const [uploading, setUploading] = useState(false); + const { uploadDefault, canUpload, uploading } = useS3(s3); const [field, meta, { setValue, setError }] = useField(id); @@ -38,14 +36,12 @@ export function ImageInput(props: ImageInputProps) { return; } try { - setUploading(true); const url = await uploadDefault(file); - setUploading(false); setValue(url); } catch (e) { setError(e.message); } - }, [ref.current, uploadDefault, canUpload, setUploading, setValue]); + }, [ref.current, uploadDefault, canUpload, setValue]); const onClick = useCallback(() => { ref.current?.click(); diff --git a/pkg/interface/src/views/components/SubmitDragger.tsx b/pkg/interface/src/views/components/SubmitDragger.tsx new file mode 100644 index 000000000..7bece2aaf --- /dev/null +++ b/pkg/interface/src/views/components/SubmitDragger.tsx @@ -0,0 +1,27 @@ +import { BaseInput, Box, Icon, LoadingSpinner, Text } from "@tlon/indigo-react"; +import React, { useCallback } from "react"; +import useS3 from "~/logic/lib/useS3"; +import { S3State } from "~/types"; + +const SubmitDragger = () => ( + + + Drop a file to upload + + +); + +export default SubmitDragger; \ No newline at end of file diff --git a/pkg/interface/src/views/components/s3-upload.tsx b/pkg/interface/src/views/components/s3-upload.tsx deleted file mode 100644 index 89b7c0627..000000000 --- a/pkg/interface/src/views/components/s3-upload.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { Component } from 'react' -import { BaseInput, Box, Text, Icon, LoadingSpinner } from "@tlon/indigo-react"; - -import S3Client from '~/logic/lib/s3'; -import { S3Credentials, S3Configuration } from '~/types'; -import { dateToDa, deSig } from '~/logic/lib/util'; - -export const SubmitDragger = () => ( - - - Drop a file to upload - - -); - -interface S3UploadProps { - credentials: S3Credentials; - configuration: S3Configuration; - uploadSuccess: Function; - uploadError: Function; - className?: string; - accept: string; -} - -interface S3UploadState { - isUploading: boolean; -} - -export class S3Upload extends Component { - private s3: S3Client; - public inputRef: React.RefObject; - - constructor(props) { - super(props); - this.state = { - isUploading: false - }; - this.s3 = new S3Client(); - this.setCredentials(props.credentials, props.configuration); - this.inputRef = React.createRef(); - this.uploadFiles = this.uploadFiles.bind(this); - } - - isReady(creds, config): boolean { - return ( - !!creds && - 'endpoint' in creds && - 'accessKeyId' in creds && - 'secretAccessKey' in creds && - creds.endpoint !== '' && - creds.accessKeyId !== '' && - creds.secretAccessKey !== '' && - !!config && - 'currentBucket' in config && - config.currentBucket !== '' - ); - } - - componentDidUpdate(prevProps): void { - const { props } = this; - if (!props.credentials !== prevProps.credentials || props.configuration !== prevProps.configuration) { - this.setCredentials(props.credentials, props.configuration); - } - } - - setCredentials(credentials, configuration): void { - if (!this.isReady(credentials, configuration)) { return; } - this.s3.setCredentials( - credentials.endpoint, - credentials.accessKeyId, - credentials.secretAccessKey - ); - } - - uploadFiles(files: FileList | File[]) { - const { props } = this; - let file = ('item' in files) ? files.item(0) : files[0]; - if (!file) { return; } - const fileParts = file.name.split('.'); - const fileName = fileParts.slice(0, -1); - const fileExtension = fileParts.pop(); - const timestamp = deSig(dateToDa(new Date())); - let bucket = props.configuration.currentBucket; - - setTimeout(() => { - if (this.state.isUploading) return; - this.setState({ isUploading: true }); - this.s3.upload(bucket, `${window.ship}/${timestamp}-${fileName}.${fileExtension}`, file) - .then((data) => { - if (!data || !('Location' in data)) { - return; - } - this.props.uploadSuccess(data.Location); - }) - .catch((err) => { - console.error(err); - this.props.uploadError(err); - }) - .finally(() => { - this.setState({ isUploading: false }); - }); - }, 200); - } - - onChange(): void { - if (!this.inputRef.current) { return; } - let files = this.inputRef.current.files; - if (!files || files.length <= 0) { return; } - this.uploadFiles(files); - } - - onClick() { - if (!this.inputRef.current) { return; } - this.inputRef.current.click(); - } - - render() { - const { - credentials, - configuration, - className = '', - accept = '*', - children = false - } = this.props; - - if (!this.isReady(credentials, configuration)) { - return null; - } - - const display = children || ; - return ( - <> - - {this.state.isUploading - ? - : {display} - } - - ); - } -} - diff --git a/pkg/interface/src/views/components/withS3.tsx b/pkg/interface/src/views/components/withS3.tsx new file mode 100644 index 000000000..5bb9bae47 --- /dev/null +++ b/pkg/interface/src/views/components/withS3.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import useS3 from "~/logic/lib/useS3"; + +const withS3 = (Component, params = {}) => { + return React.forwardRef((props: any, ref) => { + const s3 = useS3(props.s3, params); + + return ; + }); +}; + +export default withS3; \ No newline at end of file From 489a3351c427f62e4dd0ebd98099e4fb993224db Mon Sep 17 00:00:00 2001 From: Tyler Brown Cifu Shuster Date: Wed, 2 Dec 2020 07:09:12 -0800 Subject: [PATCH 4/4] s3: fixes #4039 --- pkg/interface/src/logic/lib/useDrag.ts | 2 +- pkg/interface/src/logic/lib/useS3.ts | 10 ++++++++-- .../views/apps/links/components/LinkSubmit.tsx | 18 ++++++++---------- .../apps/publish/components/MarkdownEditor.tsx | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pkg/interface/src/logic/lib/useDrag.ts b/pkg/interface/src/logic/lib/useDrag.ts index 2ec374d48..e2aa496a8 100644 --- a/pkg/interface/src/logic/lib/useDrag.ts +++ b/pkg/interface/src/logic/lib/useDrag.ts @@ -90,7 +90,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi return () => { document.body.removeEventListener('mouseout', mouseleave); } - }, [setDragging]); + }, []); const bind = { onDragLeave, diff --git a/pkg/interface/src/logic/lib/useS3.ts b/pkg/interface/src/logic/lib/useS3.ts index e6652d716..b5d4993d5 100644 --- a/pkg/interface/src/logic/lib/useS3.ts +++ b/pkg/interface/src/logic/lib/useS3.ts @@ -1,6 +1,7 @@ import { useCallback, useMemo, useEffect, useRef, useState } from "react"; import { S3State } from "../../types/s3-update"; import S3 from "aws-sdk/clients/s3"; +import { dateToDa, deSig } from "./util"; export interface IuseS3 { canUpload: boolean; @@ -37,9 +38,14 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { throw new Error("S3 not ready"); } + const fileParts = file.name.split('.'); + const fileName = fileParts.slice(0, -1); + const fileExtension = fileParts.pop(); + const timestamp = deSig(dateToDa(new Date())); + const params = { Bucket: bucket, - Key: file.name, + Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`, Body: file, ACL: "public-read", ContentType: file.type, @@ -61,7 +67,7 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { throw new Error("current bucket not set"); } return upload(file, s3.configuration.currentBucket); - }, []); + }, [s3]); const promptUpload = useCallback( () => { diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx index 601663584..ab9a78b0f 100644 --- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -87,7 +87,7 @@ const LinkSubmit = (props: LinkSubmitProps) => { }; const onFileDrag = useCallback( - async (files: FileList | File[], e: DragEvent) => { + (files: FileList | File[], e: DragEvent): void => { if (!canUpload) { return; } @@ -117,17 +117,15 @@ const LinkSubmit = (props: LinkSubmitProps) => { event.preventDefault(); event.stopPropagation(); uploadDefault(event.clipboardData.files[0]).then(setLinkValue); - }, [setLinkValue] + }, [setLinkValue, uploadDefault] ); - const onKeyPress = useCallback( - (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - doPost(); - } - }, [] - ); + const onKeyPress = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + doPost(); + } + }; const placeholder =