Skip to content

Commit 381f27c

Browse files
committed
add media attach
1 parent 0353afa commit 381f27c

4 files changed

Lines changed: 132 additions & 4 deletions

File tree

app/src/components/Composer.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
MdPermMedia,
1818
MdReply,
1919
MdRepeat,
20-
MdReplay
20+
MdReplay,
21+
MdCloudUpload
2122
} from 'react-icons/md'
2223
import { FaMarkdown } from 'react-icons/fa'
2324
import { 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

locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@
416416
"replyPlaceholder": "Write a reply...",
417417
"placeholder": "What are you up to?",
418418
"cannotPostOffline": "You can't post while offline",
419-
"resetDestination": "Reset destinations to default"
419+
"resetDestination": "Reset destinations to default",
420+
"dropToUpload": "Drop to upload"
420421
},
421422
"report": {
422423
"reason": "Report reason",

locales/ja/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@
416416
"replyPlaceholder": "返信を入力...",
417417
"placeholder": "今、なにしてる?",
418418
"cannotPostOffline": "オフラインのため投稿できません",
419-
"resetDestination": "投稿先をデフォルトに戻す"
419+
"resetDestination": "投稿先をデフォルトに戻す",
420+
"dropToUpload": "ドロップしてアップロード"
420421
},
421422
"report": {
422423
"reason": "報告理由",

web/src/components/Composer.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
MdPermMedia,
1818
MdReply,
1919
MdRepeat,
20-
MdReplay
20+
MdReplay,
21+
MdCloudUpload
2122
} from 'react-icons/md'
2223
import { FaMarkdown } from 'react-icons/fa'
2324
import { uploadImage } from '../utils/uploadImage'
@@ -94,6 +95,7 @@ export const Composer = (props: Props) => {
9495
}))
9596
})
9697
const [uploading, setUploading] = useState<boolean>(false)
98+
const [dragging, setDragging] = useState<boolean>(false)
9799
const [editorMode, setEditorMode] = useState<EditorMode>(
98100
props.draftBuffer?.editorMode ?? ((props.draftBuffer?.mediaDrafts.length ?? 0) > 0 ? 'media' : 'markdown')
99101
)
@@ -171,6 +173,11 @@ export const Composer = (props: Props) => {
171173
// inputをリセット(同じファイルを再選択可能にする)
172174
e.target.value = ''
173175

176+
await addFiles(selected)
177+
}
178+
179+
// ファイル選択・ペースト・ドロップの3入口から共通で呼ばれる受け入れ口
180+
const addFiles = async (selected: File[]) => {
174181
if (props.mode === 'reply' || editorMode === 'markdown') {
175182
// リプライ中、またはすでにドラフトがインラインでメディアを扱っている場合は
176183
// その場でアップロードしてタグを挿入する。そうでなければmediaモードへ切り替える
@@ -457,16 +464,62 @@ export const Composer = (props: Props) => {
457464
}
458465
}
459466

467+
// ドロップ受け入れは添付ボタンと同じ条件のときだけ有効化する
468+
const acceptsDrop = props.mode !== 'reroute' && displayMode !== 'plaintext'
469+
460470
return (
461471
<div
462472
style={{
473+
position: 'relative',
463474
flex: 1,
464475
minHeight: 0,
465476
display: 'flex',
466477
flexDirection: 'column',
467478
gap: CssVar.space(2)
468479
}}
480+
onDragEnter={(e) => {
481+
if (!acceptsDrop || !e.dataTransfer.types.includes('Files')) return
482+
e.preventDefault()
483+
setDragging(true)
484+
}}
485+
onDragOver={(e) => {
486+
if (!acceptsDrop || !e.dataTransfer.types.includes('Files')) return
487+
e.preventDefault()
488+
setDragging(true)
489+
}}
469490
>
491+
{/* D&D中のオーバーレイ。onDragLeaveは子要素通過でのちらつきを避けるためオーバーレイ側に付ける */}
492+
{dragging && (
493+
<div
494+
style={{
495+
position: 'absolute',
496+
inset: 0,
497+
zIndex: 1,
498+
display: 'flex',
499+
flexDirection: 'column',
500+
justifyContent: 'center',
501+
alignItems: 'center',
502+
gap: CssVar.space(1),
503+
border: '2px dashed',
504+
borderColor: CssVar.contentLink,
505+
borderRadius: CssVar.round(2),
506+
backgroundColor: CssVar.contentBackground,
507+
opacity: 0.9
508+
}}
509+
onDragLeave={(e) => {
510+
e.preventDefault()
511+
setDragging(false)
512+
}}
513+
onDrop={(e) => {
514+
e.preventDefault()
515+
setDragging(false)
516+
addFiles(Array.from(e.dataTransfer.files))
517+
}}
518+
>
519+
<MdCloudUpload size={48} color={CssVar.contentLink} />
520+
<Text style={{ margin: 0, color: CssVar.contentLink }}>{t('dropToUpload')}</Text>
521+
</div>
522+
)}
470523
<div style={{ display: 'flex', alignItems: 'flex-start', gap: CssVar.space(1) }}>
471524
{/* モードセレクタ。リプライ/リルート時は状態表示のみで切り替え不可 */}
472525
<IconButton
@@ -570,6 +623,16 @@ export const Composer = (props: Props) => {
570623
handleSubmit()
571624
}
572625
}}
626+
onPaste={(e) => {
627+
// plaintextは添付非対応なのでテキストペーストだけを通常どおり通す
628+
if (displayMode === 'plaintext') return
629+
const files = Array.from(e.clipboardData.items)
630+
.map((item) => item.getAsFile())
631+
.filter(isNonNullOrUndefined)
632+
if (files.length === 0) return
633+
e.preventDefault()
634+
addFiles(files)
635+
}}
573636
/>
574637
)}
575638

0 commit comments

Comments
 (0)