diff --git a/models/File.ts b/models/File.ts new file mode 100644 index 0000000..896e814 --- /dev/null +++ b/models/File.ts @@ -0,0 +1,30 @@ +import { toggle } from 'mobx-restful'; +import { blobOf, uniqueID } from 'web-utility'; + +import userStore from './User'; + +export interface SignedLink { + getLink: string; + putLink: string; +} + +export class FileModel { + client = userStore.client; + + @toggle('uploading') + async upload(file: string | Blob) { + if (typeof file === 'string') { + const name = file.split('/').pop()!; + + file = new File([await blobOf(file)], name); + } + const { body } = await this.client.post( + `file/signed-link/${file instanceof File ? file.name : uniqueID()}`, + ); + await this.client.put(body!.putLink, file, { 'Content-Type': file.type }); + + return body!.getLink; + } +} + +export default new FileModel(); diff --git a/pages/dashboard/project/[id].tsx b/pages/dashboard/project/[id].tsx index 42b0e11..5484c8e 100644 --- a/pages/dashboard/project/[id].tsx +++ b/pages/dashboard/project/[id].tsx @@ -4,7 +4,7 @@ import { marked } from 'marked'; import { observer } from 'mobx-react'; import { ObservedComponent, reaction } from 'mobx-react-helper'; import { compose, JWTProps, jwtVerifier, RouteProps, router } from 'next-ssr-middleware'; -import { FormEvent, KeyboardEventHandler } from 'react'; +import { ChangeEvent, ClipboardEvent, DragEvent, FormEvent, KeyboardEventHandler } from 'react'; import { formToJSON, scrollTo, sleep } from 'web-utility'; import { PageHead } from '../../../components/PageHead'; @@ -12,6 +12,7 @@ import { EvaluationDisplay } from '../../../components/Project/EvaluationDisplay import { ScrollList } from '../../../components/ScrollList'; import { SessionBox } from '../../../components/User/SessionBox'; import { ConsultMessageModel, ProjectModel } from '../../../models/ProjectEvaluation'; +import fileStore from '../../../models/File'; import { i18n, I18nContext } from '../../../models/Translation'; type ProjectEvaluationPageProps = JWTProps & RouteProps<{ id: string }>; @@ -76,6 +77,38 @@ export default class ProjectEvaluationPage extends ObservedComponent< ); }; + handleFiles = async (files: File[]) => { + for (const file of files) { + const URI = await fileStore.upload(file); + const content = file.type.startsWith('image/') + ? `![${file.name}](${URI})` + : `[${file.name}](${URI})`; + await this.messageStore.updateOne({ content }); + } + }; + + handleFileSelect = (event: ChangeEvent) => { + const files = Array.from(event.target.files || []); + if (files.length) { + this.handleFiles(files); + event.target.value = ''; + } + }; + + handlePasteDrop = (event: ClipboardEvent | DragEvent) => { + const list = + event.type === 'paste' + ? [...(event as ClipboardEvent).clipboardData.items] + : [...(event as DragEvent).dataTransfer.items]; + + const files = list.map(item => item.getAsFile()).filter((file): file is File => file !== null); + + if (files.length) { + event.preventDefault(); + this.handleFiles(files); + } + }; + renderChatMessage = ( { id, content, evaluation, prototypes, createdAt, createdBy }: ConsultMessage, index = 0, @@ -175,6 +208,10 @@ export default class ProjectEvaluationPage extends ObservedComponent< className="sticky bottom-0 mx-1 mt-auto mb-1 flex items-end gap-2 p-1.5 sm:mx-0 sm:mb-0 sm:p-2" onSubmit={this.handleMessageSubmit} > +