mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
208 lines
6.0 KiB
TypeScript
208 lines
6.0 KiB
TypeScript
import { Button } from '@heroui/button'
|
|
import type { Range } from 'quill'
|
|
import 'quill/dist/quill.core.css'
|
|
import { useRef } from 'react'
|
|
import toast from 'react-hot-toast'
|
|
|
|
import { useCustomQuill } from '@/hooks/use_custom_quill'
|
|
import useShowStructuredMessage from '@/hooks/use_show_strcuted_message'
|
|
|
|
import { quillToMessage } from '@/utils/onebot'
|
|
|
|
import type { OB11Segment } from '@/types/onebot'
|
|
|
|
import AudioInsert from './components/audio_insert'
|
|
import DiceInsert from './components/dice_insert'
|
|
import EmojiPicker from './components/emoji_picker'
|
|
import FileInsert from './components/file_insert'
|
|
import ImageInsert from './components/image_insert'
|
|
import MusicInsert from './components/music_insert'
|
|
import ReplyInsert from './components/reply_insert'
|
|
import RPSInsert from './components/rps_insert'
|
|
import VideoInsert from './components/video_insert'
|
|
import EmojiBlot from './formats/emoji_blot'
|
|
import type { EmojiValue } from './formats/emoji_blot'
|
|
import ImageBlot from './formats/image_blot'
|
|
import ReplyBlock from './formats/reply_blot'
|
|
|
|
const ChatInput = () => {
|
|
const memorizedRange = useRef<Range | null>(null)
|
|
|
|
const showStructuredMessage = useShowStructuredMessage()
|
|
const formats: string[] = ['image', 'emoji', 'reply']
|
|
const modules = {
|
|
toolbar: '#toolbar'
|
|
}
|
|
const { quillRef, quill, Quill } = useCustomQuill({
|
|
modules,
|
|
formats,
|
|
placeholder: '请输入消息'
|
|
})
|
|
|
|
if (Quill && !quill) {
|
|
Quill.register('formats/emoji', EmojiBlot)
|
|
Quill.register('formats/image', ImageBlot, true)
|
|
Quill.register('formats/reply', ReplyBlock)
|
|
}
|
|
|
|
if (quill) {
|
|
quill.on('selection-change', (range) => {
|
|
if (range) {
|
|
const editorContent = quill.getContents()
|
|
const firstOp = editorContent.ops[0]
|
|
|
|
if (
|
|
typeof firstOp?.insert !== 'string' &&
|
|
firstOp?.insert?.reply &&
|
|
range.index === 0 &&
|
|
range.length !== quill.getLength()
|
|
) {
|
|
quill.setSelection(1, Quill.sources.SILENT)
|
|
}
|
|
}
|
|
})
|
|
|
|
quill.on('text-change', () => {
|
|
const editorContent = quill.getContents()
|
|
const firstOp = editorContent.ops[0]
|
|
if (
|
|
firstOp &&
|
|
typeof firstOp.insert !== 'string' &&
|
|
firstOp.insert?.reply &&
|
|
quill.getLength() === 1
|
|
) {
|
|
quill.insertText(1, '\n', Quill.sources.SILENT)
|
|
}
|
|
})
|
|
|
|
quill.on('editor-change', (eventName: string) => {
|
|
if (eventName === 'text-change') {
|
|
const editorContent = quill.getContents()
|
|
const firstOp = editorContent.ops[0]
|
|
if (
|
|
firstOp &&
|
|
typeof firstOp.insert !== 'string' &&
|
|
firstOp.insert?.reply &&
|
|
quill.getLength() === 1
|
|
) {
|
|
quill.insertText(1, '\n', Quill.sources.SILENT)
|
|
}
|
|
}
|
|
})
|
|
|
|
quill.root.addEventListener('compositionstart', () => {
|
|
const editorContent = quill.getContents()
|
|
const firstOp = editorContent.ops[0]
|
|
if (
|
|
firstOp &&
|
|
typeof firstOp.insert !== 'string' &&
|
|
firstOp.insert?.reply &&
|
|
quill.getLength() === 1
|
|
) {
|
|
quill.insertText(1, '\n', Quill.sources.SILENT)
|
|
}
|
|
})
|
|
}
|
|
|
|
const onOpenChange = (open: boolean) => {
|
|
if (open) {
|
|
const selection = quill?.getSelection()
|
|
if (selection) memorizedRange.current = selection
|
|
}
|
|
}
|
|
|
|
const insertImage = (url: string) => {
|
|
const selection = memorizedRange.current || quill?.getSelection()
|
|
quill?.deleteText(selection?.index || 0, selection?.length || 0)
|
|
quill?.insertEmbed(selection?.index || 0, 'image', {
|
|
src: url,
|
|
alt: '图片'
|
|
})
|
|
quill?.setSelection((selection?.index || 0) + 1, 0)
|
|
}
|
|
function insertReplyBlock(messageId: string) {
|
|
const isNumberReg = /^(?:0|(?:-?[1-9]\d*))$/
|
|
if (!isNumberReg.test(messageId)) {
|
|
toast.error('请输入正确的消息ID')
|
|
return
|
|
}
|
|
const editorContent = quill?.getContents()
|
|
const firstOp = editorContent?.ops[0]
|
|
const currentSelection = quill?.getSelection()
|
|
if (
|
|
firstOp &&
|
|
typeof firstOp.insert !== 'string' &&
|
|
firstOp.insert?.reply
|
|
) {
|
|
const delta = quill?.getContents()
|
|
if (delta) {
|
|
delta.ops[0] = {
|
|
insert: { reply: { messageId } }
|
|
}
|
|
quill?.setContents(delta, Quill.sources.USER)
|
|
}
|
|
} else {
|
|
quill?.insertEmbed(0, 'reply', { messageId }, Quill.sources.USER)
|
|
}
|
|
quill?.setSelection((currentSelection?.index || 0) + 1, 0)
|
|
quill?.blur()
|
|
}
|
|
const onInsertEmoji = (emoji: EmojiValue) => {
|
|
const selection = memorizedRange.current || quill?.getSelection()
|
|
quill?.deleteText(selection?.index || 0, selection?.length || 0)
|
|
quill?.insertEmbed(selection?.index || 0, 'emoji', {
|
|
alt: emoji.alt,
|
|
src: emoji.src,
|
|
id: emoji.id
|
|
})
|
|
quill?.setSelection((selection?.index || 0) + 1, 0)
|
|
}
|
|
|
|
const getChatMessage = () => {
|
|
const delta = quill?.getContents()
|
|
const ops =
|
|
delta?.ops?.filter((op) => {
|
|
return op.insert !== '\n'
|
|
}) ?? []
|
|
const messages: OB11Segment[] = ops.map((op) => {
|
|
return quillToMessage(op)
|
|
})
|
|
return messages
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
ref={quillRef}
|
|
className="border border-default-200 rounded-md !mb-2 !text-base !h-64"
|
|
/>
|
|
<div id="toolbar" className="!border-none flex gap-2">
|
|
<ImageInsert insertImage={insertImage} onOpenChange={onOpenChange} />
|
|
<EmojiPicker
|
|
onInsertEmoji={onInsertEmoji}
|
|
onOpenChange={onOpenChange}
|
|
/>
|
|
<ReplyInsert insertReply={insertReplyBlock} />
|
|
<FileInsert />
|
|
<AudioInsert />
|
|
<VideoInsert />
|
|
<MusicInsert />
|
|
<DiceInsert />
|
|
<RPSInsert />
|
|
<Button
|
|
color="primary"
|
|
onPress={() => {
|
|
const messages = getChatMessage()
|
|
showStructuredMessage(messages)
|
|
}}
|
|
className="ml-auto"
|
|
>
|
|
获取JSON格式
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ChatInput
|