@@ -17,7 +17,8 @@ import {
1717 MdPermMedia ,
1818 MdReply ,
1919 MdRepeat ,
20- MdReplay
20+ MdReplay ,
21+ MdCloudUpload
2122} from 'react-icons/md'
2223import { FaMarkdown } from 'react-icons/fa'
2324import { uploadImage } from '../utils/uploadImage'
@@ -92,6 +93,7 @@ export const Composer = (props: Props) => {
9293 } ) )
9394 } )
9495 const [ uploading , setUploading ] = useState < boolean > ( false )
96+ const [ dragging , setDragging ] = useState < boolean > ( false )
9597 const [ editorMode , setEditorMode ] = useState < EditorMode > (
9698 props . draftBuffer ?. editorMode ?? ( ( props . draftBuffer ?. mediaDrafts . length ?? 0 ) > 0 ? 'media' : 'markdown' )
9799 )
@@ -169,6 +171,11 @@ export const Composer = (props: Props) => {
169171 // inputをリセット(同じファイルを再選択可能にする)
170172 e . target . value = ''
171173
174+ await addFiles ( selected )
175+ }
176+
177+ // ファイル選択・ペースト・ドロップの3入口から共通で呼ばれる受け入れ口
178+ const addFiles = async ( selected : File [ ] ) => {
172179 if ( props . mode === 'reply' || editorMode === 'markdown' ) {
173180 // リプライ中、またはすでにドラフトがインラインでメディアを扱っている場合は
174181 // その場でアップロードしてタグを挿入する。そうでなければmediaモードへ切り替える
@@ -455,16 +462,62 @@ export const Composer = (props: Props) => {
455462 }
456463 }
457464
465+ // ドロップ受け入れは添付ボタンと同じ条件のときだけ有効化する
466+ const acceptsDrop = props . mode !== 'reroute' && displayMode !== 'plaintext'
467+
458468 return (
459469 < div
460470 style = { {
471+ position : 'relative' ,
461472 flex : 1 ,
462473 minHeight : 0 ,
463474 display : 'flex' ,
464475 flexDirection : 'column' ,
465476 gap : CssVar . space ( 2 )
466477 } }
478+ onDragEnter = { ( e ) => {
479+ if ( ! acceptsDrop || ! e . dataTransfer . types . includes ( 'Files' ) ) return
480+ e . preventDefault ( )
481+ setDragging ( true )
482+ } }
483+ onDragOver = { ( e ) => {
484+ if ( ! acceptsDrop || ! e . dataTransfer . types . includes ( 'Files' ) ) return
485+ e . preventDefault ( )
486+ setDragging ( true )
487+ } }
467488 >
489+ { /* D&D中のオーバーレイ。onDragLeaveは子要素通過でのちらつきを避けるためオーバーレイ側に付ける */ }
490+ { dragging && (
491+ < div
492+ style = { {
493+ position : 'absolute' ,
494+ inset : 0 ,
495+ zIndex : 1 ,
496+ display : 'flex' ,
497+ flexDirection : 'column' ,
498+ justifyContent : 'center' ,
499+ alignItems : 'center' ,
500+ gap : CssVar . space ( 1 ) ,
501+ border : '2px dashed' ,
502+ borderColor : CssVar . contentLink ,
503+ borderRadius : CssVar . round ( 2 ) ,
504+ backgroundColor : CssVar . contentBackground ,
505+ opacity : 0.9
506+ } }
507+ onDragLeave = { ( e ) => {
508+ e . preventDefault ( )
509+ setDragging ( false )
510+ } }
511+ onDrop = { ( e ) => {
512+ e . preventDefault ( )
513+ setDragging ( false )
514+ addFiles ( Array . from ( e . dataTransfer . files ) )
515+ } }
516+ >
517+ < MdCloudUpload size = { 48 } color = { CssVar . contentLink } />
518+ < Text style = { { margin : 0 , color : CssVar . contentLink } } > { t ( 'dropToUpload' ) } </ Text >
519+ </ div >
520+ ) }
468521 < div style = { { display : 'flex' , alignItems : 'flex-start' , gap : CssVar . space ( 1 ) } } >
469522 { /* モードセレクタ。リプライ/リルート時は状態表示のみで切り替え不可 */ }
470523 < IconButton
@@ -564,6 +617,16 @@ export const Composer = (props: Props) => {
564617 handleSubmit ( )
565618 }
566619 } }
620+ onPaste = { ( e ) => {
621+ // plaintextは添付非対応なのでテキストペーストだけを通常どおり通す
622+ if ( displayMode === 'plaintext' ) return
623+ const files = Array . from ( e . clipboardData . items )
624+ . map ( ( item ) => item . getAsFile ( ) )
625+ . filter ( isNonNullOrUndefined )
626+ if ( files . length === 0 ) return
627+ e . preventDefault ( )
628+ addFiles ( files )
629+ } }
567630 />
568631 ) }
569632
0 commit comments