Files
NapCatQQ/napcat.webui/src/components/chat_input/index.tsx
2025-11-03 12:43:33 +08:00

208 lines
6.1 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;