You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
14 KiB
JavaScript
333 lines
14 KiB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { App, Input, Flex, Button, Image, } from 'antd';
|
|
// import { Input } from 'react-chat-elements';
|
|
import useAuthStore from '@/stores/AuthStore';
|
|
import useConversationStore from '@/stores/ConversationStore';
|
|
import {
|
|
SendOutlined,
|
|
MessageOutlined,
|
|
SmileOutlined,
|
|
PictureOutlined,
|
|
CommentOutlined,
|
|
UploadOutlined,
|
|
CloudUploadOutlined,
|
|
FolderAddOutlined,
|
|
FilePdfOutlined,
|
|
CloseCircleOutlined,
|
|
YoutubeOutlined,
|
|
AudioOutlined, PlayCircleOutlined, LoadingOutlined, CheckCircleOutlined, FileOutlined
|
|
} from '@ant-design/icons';
|
|
import { isEmpty, } from '@/utils/commons';
|
|
import { v4 as uuid } from 'uuid';
|
|
import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/whatsappUtils';
|
|
import InputTemplate from './Template';
|
|
import InputEmoji from './Emoji';
|
|
import InputMediaUpload from './MediaUpload';
|
|
import { OSS_URL as aliOSSHost } from '@/config';
|
|
import { postUploadFileItem } from '@/actions/CommonActions';
|
|
import ExpireTimeClock from '../ExpireTimeClock';
|
|
import dayjs from 'dayjs';
|
|
|
|
const ButtonStyleClsMapped =
|
|
{
|
|
'waba': 'bg-waba shadow shadow-waba-300 hover:!bg-waba-400 active:bg-waba-400 focus:bg-waba-400',
|
|
'whatsapp': 'bg-whatsapp shadow shadow-whatsapp-300 hover:!bg-whatsapp-400 active:bg-whatsapp-400 focus:bg-whatsapp-400',
|
|
};
|
|
|
|
const InputComposer = ({ mobile, isWABA, channel }) => {
|
|
const userId = useAuthStore((state) => state.loginUser.userId);
|
|
const websocket = useConversationStore((state) => state.websocket);
|
|
const websocketOpened = useConversationStore((state) => state.websocketOpened);
|
|
const currentConversation = useConversationStore((state) => state.currentConversation);
|
|
const [referenceMsg, setReferenceMsg] = useConversationStore((state) => [state.referenceMsg, state.setReferenceMsg]);
|
|
const [complexMsg, setComplexMsg] = useConversationStore((state) => [state.complexMsg, state.setComplexMsg]);
|
|
const sentOrReceivedNewMessage = useConversationStore((state) => state.sentOrReceivedNewMessage);
|
|
|
|
const talkabled = !isEmpty(currentConversation.sn) && websocketOpened;
|
|
const isExpired = !isEmpty(currentConversation.conversation_expiretime) ? dayjs(currentConversation.conversation_expiretime).add(8, 'hours').isBefore(dayjs()) : true;
|
|
const gt24h = !isEmpty(currentConversation.last_received_time) ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true;
|
|
const lt24h = !isEmpty(currentConversation.last_received_time) ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') <= 24 : false;
|
|
// lt24h || !isExpired
|
|
const textabled = talkabled; // && (lt24h || !isExpired); // 只要有一个时间没过期, 目前未知明确规则
|
|
const textabled0 = talkabled && (lt24h || !isExpired); // 只要有一个时间没过期, 目前未知明确规则
|
|
// debug: 日志
|
|
// console.group('InputComposer textabled');
|
|
// console.log('c_sn, websocketOpened, lt24h, isExpired, textabled', currentConversation.sn, websocketOpened, lt24h, isExpired, textabled);
|
|
// console.log('received time, expire time', currentConversation.last_received_time, ', ', currentConversation.conversation_expiretime);
|
|
if (!isEmpty(currentConversation.sn) && !textabled) {
|
|
// console.log('current chat: ---- \n', JSON.stringify(currentConversation, null, 2));
|
|
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
|
|
}
|
|
// console.groupEnd();
|
|
const textPlaceHolder = !textabled
|
|
? ''
|
|
: mobile === undefined
|
|
? 'Enter 发送, Shift+Enter 换行'
|
|
: 'Enter 换行';
|
|
|
|
const textInputRef = useRef(null);
|
|
const [textContent, setTextContent] = useState('');
|
|
|
|
const invokeSendMessage = (msgObj) => {
|
|
const msgObjMerge = {
|
|
sender: 'me',
|
|
senderName: 'me',
|
|
to: currentConversation.whatsapp_phone_number,
|
|
date: new Date(),
|
|
status: 'waiting',
|
|
...(referenceMsg.id ? { context: { message_id: referenceMsg.id }, message_origin: referenceMsg } : {}),
|
|
...msgObj,
|
|
id: `${currentConversation.sn}.${uuid()}`,
|
|
};
|
|
// olog('sendMessage------------------', msgObjMerge)
|
|
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
|
|
// console.log('content to send-------------------------------------', contentToSend);
|
|
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn, conversationid: currentConversation.sn, });
|
|
const contentToRender = sentMsgTypeMapped[msgObjMerge.type].contentToRender(msgObjMerge);
|
|
// console.log(contentToRender, 'contentToRender sendMessage------------------');
|
|
sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
|
|
|
|
setTextContent('');
|
|
setReferenceMsg({});
|
|
setComplexMsg({});
|
|
};
|
|
|
|
/**
|
|
* 先推到消息记录上面, 再发送
|
|
*/
|
|
const invokeUploadFileMessage = (msgObj) => {
|
|
const msgObjMerge = {
|
|
sender: 'me',
|
|
senderName: 'me',
|
|
to: currentConversation.whatsapp_phone_number,
|
|
date: new Date(),
|
|
status: 'waiting',
|
|
...(referenceMsg.id ? { context: { message_id: referenceMsg.id }, message_origin: referenceMsg } : {}),
|
|
...msgObj,
|
|
id: `${currentConversation.sn}.${msgObj.id}`,
|
|
};
|
|
// olog('invoke upload', msgObjMerge)
|
|
const contentToRender = sentMsgTypeMapped[msgObjMerge.type].contentToRender(msgObjMerge);
|
|
// console.log(contentToRender, 'contentToRender sendMessage------------------');
|
|
sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
|
|
};
|
|
|
|
const invokeSendUploadMessage = (msgObj) => {
|
|
const msgObjMerge = {
|
|
sender: 'me',
|
|
senderName: 'me',
|
|
to: currentConversation.whatsapp_phone_number,
|
|
date: new Date(),
|
|
status: 'waiting',
|
|
...(referenceMsg.id ? { context: { message_id: referenceMsg.id }, message_origin: referenceMsg } : {}),
|
|
...msgObj,
|
|
id: `${currentConversation.sn}.${msgObj.id}`,
|
|
};
|
|
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
|
|
// olog('invoke upload send +++ ', contentToSend)
|
|
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn, conversationid: currentConversation.sn, });
|
|
};
|
|
|
|
const { message: appMessage } = App.useApp();
|
|
const [pastedUploading, setPastedUploading] = useState(false);
|
|
const readPasted = async (file) => {
|
|
const fileTypeSupport = Object.keys(whatsappSupportFileTypes).find((msgType) => whatsappSupportFileTypes[msgType].types.includes(file.type));
|
|
if (isEmpty(fileTypeSupport)) {
|
|
appMessage.warning('不支持的粘贴内容');
|
|
return false;
|
|
}
|
|
const waFile = whatsappSupportFileTypes[fileTypeSupport];
|
|
if (file.size > waFile.size) {
|
|
appMessage.warning('超过大小限制');
|
|
return false;
|
|
}
|
|
// 使用 FileReader 读取文件对象
|
|
const reader = new FileReader();
|
|
const suffix = file.name.slice(file.name.lastIndexOf('.') + 1).toLocaleLowerCase();
|
|
const newName = `${uuid()}.${suffix}`; // rename ? `${uuid()}.${suffix}` : file.name;
|
|
const dataUri = aliOSSHost + newName;
|
|
const msgObj = {
|
|
type: fileTypeSupport,
|
|
name: file.name,
|
|
uploadStatus: 'loading',
|
|
data: { dataUri: '', link: '', width: '100%', height: 150, loading: uploadProgressSimulate() },
|
|
id: uuid(),
|
|
};
|
|
// 读取完毕后获取结果
|
|
reader.onload = (event) => {
|
|
const previewSrc = event.target.result;
|
|
msgObj.data.uri = previewSrc;
|
|
};
|
|
file.newName = newName;
|
|
file.msgData = msgObj;
|
|
// 把文件对象作为一个 dataURL 读入
|
|
reader.readAsDataURL(file);
|
|
return file;
|
|
};
|
|
const handlePaste = async (event) => {
|
|
const items = (event.clipboardData || window.clipboardData).items;
|
|
let tmpfile = null;
|
|
if (!items || items.length === 0) {
|
|
// 当前浏览器不支持本地
|
|
appMessage.warning('当前浏览器不支持本地');
|
|
return;
|
|
}
|
|
let isNotFile = true;
|
|
for (let i = 0; i < items.length; i++) {
|
|
// if (items[i].type.indexOf("image") !== -1) {
|
|
if (items[i].kind.indexOf('file') !== -1) {
|
|
isNotFile = false;
|
|
tmpfile = items[i].getAsFile();
|
|
break;
|
|
}
|
|
}
|
|
if (isNotFile) {
|
|
// 普通的粘贴
|
|
return;
|
|
}
|
|
if (!tmpfile) {
|
|
appMessage.warning('没有读取到粘贴内容');
|
|
return;
|
|
}
|
|
const shouldRename = tmpfile.type.indexOf('image') !== -1;
|
|
const _tmpFile = await readPasted(tmpfile, shouldRename);
|
|
if (_tmpFile === false) {
|
|
return;
|
|
}
|
|
setComplexMsg(_tmpFile.msgData);
|
|
setPastedUploading(true);
|
|
const { file_url } = await postUploadFileItem(tmpfile, _tmpFile.newName);
|
|
setPastedUploading(false); // todo: 更新data uri
|
|
if (file_url) {
|
|
_tmpFile.msgData.data.dataUri = file_url;
|
|
_tmpFile.msgData.data.link = file_url;
|
|
// _tmpFile.msgData.data.uri = file_url;
|
|
}
|
|
setComplexMsg({ ..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error' });
|
|
return;
|
|
};
|
|
|
|
const focusInput = () => {
|
|
textInputRef.current.focus({ cursor: 'end', preventScroll: true });
|
|
window.dispatchEvent(new Event('resize'));
|
|
};
|
|
|
|
const addEmoji = (emoji) => {
|
|
setTextContent((prevValue) => {
|
|
return prevValue + emoji;
|
|
});
|
|
};
|
|
|
|
const handleSendText = () => {
|
|
if (textContent.trim() !== '' || !isEmpty(complexMsg)) {
|
|
const msgObj = {
|
|
type: 'text',
|
|
text: textContent,
|
|
...complexMsg,
|
|
};
|
|
invokeSendMessage(msgObj);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
focusInput();
|
|
return () => {};
|
|
}, [referenceMsg, complexMsg]);
|
|
|
|
return (
|
|
<div>
|
|
{referenceMsg.id && (
|
|
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
|
|
<div className='flex flex-col referrer-msg border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'>
|
|
<span className=' text-primary pr-1 italic align-top'>{referenceMsg.senderName}</span>
|
|
{referenceMsg.type === 'photo' && <Image width={100} src={referenceMsg.data.uri} />}
|
|
<span className='px-1 whitespace-pre-wrap'>{referenceMsg.originText}</span>
|
|
</div>
|
|
<Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setReferenceMsg({})} />
|
|
</Flex>
|
|
)}
|
|
{complexMsg.id && (
|
|
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
|
|
<div className='pl-2 pr-1 py-1'>
|
|
{['photo', 'sticker'].includes(complexMsg.type) && complexMsg.data.uri ? (
|
|
<Image width={100} src={complexMsg.data.uri} />
|
|
) : (
|
|
<>
|
|
<FileOutlined className=' text-red-400' />
|
|
<span className='px-1'>{complexMsg.name}</span>
|
|
</>
|
|
)}
|
|
{complexMsg.uploadStatus === 'loading' && <LoadingOutlined className='px-1' />}
|
|
{/* {complexMsg.uploadStatus === 'done' && <CheckCircleOutlined className='px-1 text-primary' />} */}
|
|
{complexMsg.uploadStatus === 'error' && (
|
|
<>
|
|
<CloseCircleOutlined className='px-1 text-red-400' /> <span>添加失败</span>{' '}
|
|
</>
|
|
)}
|
|
</div>
|
|
<Button type='text' title='删除' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setComplexMsg({})} />
|
|
</Flex>
|
|
)}
|
|
<Input.TextArea
|
|
onPaste={handlePaste}
|
|
ref={textInputRef}
|
|
size='large'
|
|
maxLength={complexMsg.id ? 1024 : 2000}
|
|
showCount={textabled}
|
|
placeholder={
|
|
!talkabled
|
|
? '请先选择会话'
|
|
: !textabled0 && isWABA
|
|
? '会话已超24h不活跃. 请发送打招呼消息激活对话💬.'
|
|
: mobile === undefined
|
|
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
|
|
: 'Enter 换行, 点击 Send 发送'
|
|
}
|
|
rows={2}
|
|
disabled={!textabled}
|
|
value={textContent}
|
|
onChange={(e) => setTextContent(e.target.value)}
|
|
className='rounded-b-none emoji'
|
|
onPressEnter={(e) => {
|
|
if (!e.shiftKey && mobile === undefined) {
|
|
e.preventDefault();
|
|
if (textabled && !pastedUploading) handleSendText();
|
|
}
|
|
}}
|
|
autoSize={{ minRows: 2, maxRows: 6 }}
|
|
/>
|
|
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b-0'>
|
|
<Flex gap={4} className='*:text-primary *:rounded-none items-center'>
|
|
{isWABA && <InputTemplate key='templates' disabled={!talkabled} invokeSendMessage={invokeSendMessage} {...{ mobile }} />}
|
|
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} {...{ mobile }} />
|
|
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
|
|
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
|
|
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} />
|
|
<Button type='text' className='' icon={<FolderAddOutlined />} size={'middle'} />
|
|
<Button type='text' className='' icon={<CloudUploadOutlined />} size={'middle'} />
|
|
<Button type='text' className='' icon={<FilePdfOutlined />} size={'middle'} /> */}
|
|
</Flex>
|
|
<Flex gap={4} align={'center'}>
|
|
<div className='text-neutral-400 text-sm'>
|
|
{/* <ExpireTimeClock expireTime={currentConversation.conversation_expiretime} /> */}
|
|
<div>{textPlaceHolder}</div>
|
|
</div>
|
|
<Button
|
|
key={'send-btn'}
|
|
onClick={handleSendText}
|
|
type='primary'
|
|
size='middle'
|
|
icon={<SendOutlined />}
|
|
disabled={!textabled || pastedUploading}
|
|
className={ButtonStyleClsMapped[channel]
|
|
}>
|
|
Send
|
|
</Button>
|
|
</Flex>
|
|
</Flex>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InputComposer;
|