feat: 消息引用: 图片; 视频; 消息类型: 文件: 点击打开; 滚动到底部 fix: 模板激活状态

dev/mobile
Lei OT 2 years ago
parent ba10f21641
commit 0900cb049f

@ -48,6 +48,7 @@ const mediaMsg = {
...(msg.context ...(msg.context
? { ? {
reply: { reply: {
id: msg.message_origin.id,
message: msg.message_origin.text, message: msg.message_origin.text,
title: msg.message_origin.senderName || 'Reference', title: msg.message_origin.senderName || 'Reference',
titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e', titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e',
@ -76,10 +77,11 @@ export const sentMsgTypeMapped = {
...(msg.context ...(msg.context
? { ? {
reply: { reply: {
id: msg.message_origin.id,
message: msg.message_origin.text, message: msg.message_origin.text,
title: msg.message_origin.senderName || 'Reference', title: msg.message_origin.senderName || 'Reference',
titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e', titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e',
// titleColor: "#a791ff", photoURL: msg.message_origin?.data?.uri || '',
}, },
} }
: {}), : {}),
@ -211,6 +213,7 @@ export const whatsappMsgTypeMapped = {
id: msg.wamid, id: msg.wamid,
text: msg.image.caption, text: msg.image.caption,
data: { id: msg.wamid, uri: msg.image.link, width: '100%', height: 200, alt: msg.image.caption, }, data: { id: msg.wamid, uri: msg.image.link, width: '100%', height: 200, alt: msg.image.caption, },
originText: msg.image?.caption || '',
onOpen: () => { onOpen: () => {
console.log('Open image', msg.image.link); console.log('Open image', msg.image.link);
}, },
@ -225,6 +228,9 @@ export const whatsappMsgTypeMapped = {
id: msg.wamid, id: msg.wamid,
data: { id: msg.wamid, uri: msg.sticker.link, width: '100%', height: 120, alt: '' }, data: { id: msg.wamid, uri: msg.sticker.link, width: '100%', height: 120, alt: '' },
}), }),
renderForReply: (msg) => ({
id: msg.wamid, photoURL: msg.sticker.link, width: '100%', height: 200, alt: '', message: '[表情]'
}),
}, },
video: { video: {
type: 'video', type: 'video',
@ -241,7 +247,7 @@ export const whatsappMsgTypeMapped = {
}, },
}), }),
renderForReply: (msg) => ({ renderForReply: (msg) => ({
id: msg.wamid, videoURL: msg.video.link, width: 200, height: 200, alt: '', id: msg.wamid, videoURL: msg.video.link, photoURL: msg.video.link, message: msg.video?.caption || '[视频]', width: 200, height: 200, alt: '',
}), }),
}, },
audio: { audio: {
@ -253,14 +259,18 @@ export const whatsappMsgTypeMapped = {
}, },
}), }),
}, },
unsupported: { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) }, // unsupported: { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) },
unsupported: { type: 'text', data: (msg) => ({ text: '[暂不支持此消息类型]' }) },
reaction: { reaction: {
type: 'text', type: 'text',
data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '', }), data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '', }),
}, },
document: { document: {
type: 'file', type: 'file',
data: (msg) => ({ id: msg.wamid, text: msg.document.filename, data: { uri: msg.document.link, extension: 'PDF', status: { click: false, loading: 0, } } }), data: (msg) => ({ id: msg.wamid, title: msg.document?.filename || '', text: msg.document?.caption || msg.document?.filename || '', data: { uri: msg.document.link, extension: 'PDF', status: { click: false, download: true, loading: 0, } }, originText: msg.document?.caption || msg.document?.filename || '', }),
renderForReply: (msg) => ({
id: msg.wamid, message: msg.document?.caption || msg.document?.filename || '',
}),
}, },
// location: 'location', // location: 'location',
// contact: 'contact', // contact: 'contact',

@ -2,7 +2,7 @@ import { create } from 'zustand';
import { RealTimeAPI } from '@/lib/realTimeAPI'; import { RealTimeAPI } from '@/lib/realTimeAPI';
import { olog, isEmpty } from '@/utils/utils'; import { olog, isEmpty } from '@/utils/utils';
import { receivedMsgTypeMapped, handleNotification } from '@/lib/msgUtils'; import { receivedMsgTypeMapped, handleNotification } from '@/lib/msgUtils';
import { fetchConversationsList, fetchTemplates } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchTemplates, fetchMessages } from '@/actions/ConversationActions';
import { devtools } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
import { WS_URL } from '@/config'; import { WS_URL } from '@/config';
@ -168,7 +168,7 @@ const conversationSlice = (set, get) => ({
}, },
setCurrentConversation: (conversation) => { setCurrentConversation: (conversation) => {
// 清空未读 // 清空未读
const { conversationsList, totalNotify } = get(); const { conversationsList } = get();
const targetId = conversation.sn; const targetId = conversation.sn;
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId)); const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
targetIndex !== -1 targetIndex !== -1
@ -178,7 +178,12 @@ const conversationSlice = (set, get) => ({
}) })
: null; : null;
return set({ totalNotify: totalNotify - (conversation.unread_msg_count || 0), currentConversation: conversation, referenceMsg: {}, conversationsList: [...conversationsList] }); return set((state) => ({
totalNotify: state.totalNotify - (conversation.unread_msg_count || 0),
currentConversation: conversation,
referenceMsg: {},
conversationsList: [...conversationsList],
}));
}, },
}); });
@ -189,7 +194,7 @@ const messageSlice = (set, get) => ({
setMsgLoading: (msgListLoading) => set({ msgListLoading }), setMsgLoading: (msgListLoading) => set({ msgListLoading }),
receivedMessageList: (conversationid, msgList) => receivedMessageList: (conversationid, msgList) =>
set((state) => ({ set((state) => ({
msgListLoading: false, // msgListLoading: false,
activeConversations: { ...state.activeConversations, [String(conversationid)]: msgList }, activeConversations: { ...state.activeConversations, [String(conversationid)]: msgList },
})), })),
updateMessageItem: (message) => { updateMessageItem: (message) => {
@ -280,7 +285,7 @@ export const useConversationStore = create(
// side effects // side effects
fetchInitialData: async (userId) => { fetchInitialData: async (userId) => {
const { addToConversationList, setTemplates, setInitial } = get(); const { addToConversationList, setTemplates, setInitial, receivedMessageList } = get();
const conversationsList = await fetchConversationsList({ opisn: userId }); const conversationsList = await fetchConversationsList({ opisn: userId });
addToConversationList(conversationsList); addToConversationList(conversationsList);
@ -289,6 +294,11 @@ export const useConversationStore = create(
setTemplates(templates); setTemplates(templates);
setInitial(true); setInitial(true);
for (const chatItem of conversationsList) {
const msgData = await fetchMessages({ opisn: chatItem.opi_sn, whatsappid: chatItem.whatsapp_phone_number });
receivedMessageList(chatItem.sn, msgData);
}
}, },
})) }))
); );

@ -50,10 +50,8 @@ const ChatWindow = () => {
{/* <Button type='text' icon={<ReloadOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className='' title='最新消息记录' /> */} {/* <Button type='text' icon={<ReloadOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className='' title='最新消息记录' /> */}
<Button type='text' icon={collapsedRight ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className=' rounded-none rounded-r' /> <Button type='text' icon={collapsedRight ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className=' rounded-none rounded-r' />
</Header> </Header>
<Content className="flex-grow bg-whatsapp-bg" > <Content className="flex-grow bg-whatsapp-bg relative" >
<div className='h-full overflow-y-auto'> <Messages />
<Messages />
</div>
</Content> </Content>
<Footer className='ant-layout-sider-light p-0'> <Footer className='ant-layout-sider-light p-0'>
<InputComposer /> <InputComposer />

@ -59,6 +59,7 @@ const Conversations = () => {
const getMessages = async (item) => { const getMessages = async (item) => {
setMsgLoading(true); setMsgLoading(true);
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number }); const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number });
setMsgLoading(false);
receivedMessageList(item.sn, data); receivedMessageList(item.sn, data);
}; };
const switchConversation = async (item) => { const switchConversation = async (item) => {

@ -16,7 +16,7 @@ const InputTemplate = ({ disabled = false, inputEmoji }) => {
placement={'right'} placement={'right'}
overlayInnerStyle={{ padding: 0, borderRadius: '8px' }} overlayInnerStyle={{ padding: 0, borderRadius: '8px' }}
// fresh // fresh
content={<EmojiPicker skinTonesDisabled={true} emojiStyle='apple' onEmojiClick={handlePickEmoji} />} content={<EmojiPicker skinTonesDisabled={true} emojiStyle='google' onEmojiClick={handlePickEmoji} />}
// title='😀' // title='😀'
trigger='click' trigger='click'
open={openPopup} open={openPopup}

@ -97,8 +97,9 @@ const InputComposer = () => {
{referenceMsg.id && ( {referenceMsg.id && (
<Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'> <Flex justify='space-between' className='reply-to bg-gray-100 p-1 rounded-none text-slate-500'>
<div className='referrer-msg border-l-3 border-y-0 border-r-0 border-slate-300 border-solid pl-2 pr-1 py-1'> <div className='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'>{referenceMsg.senderName}</span> <span className=' text-primary pr-1 italic align-top'>{referenceMsg.senderName}</span>
{referenceMsg.originText} {referenceMsg.type === 'photo' && <Image width={100} src={referenceMsg.data.uri} />}
<span className='px-1'>{referenceMsg.originText}</span>
</div> </div>
<Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setReferenceMsg({})} /> <Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setReferenceMsg({})} />
</Flex> </Flex>
@ -134,7 +135,7 @@ const InputComposer = () => {
/> />
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'> <Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'>
<Flex gap={4} className='*:text-primary *:rounded-none'> <Flex gap={4} className='*:text-primary *:rounded-none'>
<InputTemplate key='templates' disabled={textabled} invokeSendMessage={invokeSendMessage} /> <InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} />
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} /> <InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} />
{/* <InputImageUpload key={'addNewPic'} disabled={!textabled} invokeSendMessage={invokeSendMessage} /> */} {/* <InputImageUpload key={'addNewPic'} disabled={!textabled} invokeSendMessage={invokeSendMessage} /> */}
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} /> {/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />

@ -1,13 +1,13 @@
import { useEffect, useState, useRef, useMemo, memo, createRef, forwardRef } from 'react'; import { useEffect, useState, useRef, useMemo, memo, createRef, forwardRef } from 'react';
import { Image, Spin, Dropdown } from 'antd'; import { Image, Spin, Dropdown, Button, Affix } from 'antd';
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements'; import { MessageBox } from 'react-chat-elements';
import useConversationStore from '@/stores/ConversationStore'; import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { Emoji } from 'emoji-picker-react'; import { Emoji } from 'emoji-picker-react';
import { olog } from '@/utils/utils'; import { isEmpty, olog } from '@/utils/utils';
const Messages = () => { const Messages = ({ ...props }) => {
// const currentConversation = useConversationStore(useShallow((state) => state.currentConversation)); // const currentConversation = useConversationStore(useShallow((state) => state.currentConversation));
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg)); const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
const msgListLoading = useConversationStore(useShallow((state) => state.msgListLoading)); const msgListLoading = useConversationStore(useShallow((state) => state.msgListLoading));
@ -17,19 +17,26 @@ const Messages = () => {
const scrollToMessage = (id, index) => { const scrollToMessage = (id, index) => {
const _i = index || activeMessages.findIndex((msg) => msg.id === id); const _i = index || activeMessages.findIndex((msg) => msg.id === id);
if (_i >= 0) { if (_i >= 0) {
messageRefs.current[_i].current.scrollIntoView({ behavior: "smooth", block: "start" }); messageRefs.current[_i].current.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
} };
const messageRefs = useRef([]);
messageRefs.current = activeMessages.map((_, i) => messageRefs.current[i] ?? createRef());
const referance = useRef(null);
const toBottom = (e) => {
if (!referance) return;
referance.current.scrollTop = referance.current.scrollHeight;
};
useEffect(() => { useEffect(() => {
if (activeMessages.length > 0) { if (activeMessages.length > 0) {
// toBottom();
scrollToMessage(null, activeMessages.length - 1); scrollToMessage(null, activeMessages.length - 1);
} }
}, [activeMessages]); }, [activeMessages]);
const messageRefs = useRef([]);
messageRefs.current = activeMessages.map((_, i) => messageRefs.current[i] ?? createRef());
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
const [previewSrc, setPreviewSrc] = useState(); const [previewSrc, setPreviewSrc] = useState();
const onPreviewClose = () => { const onPreviewClose = () => {
@ -37,15 +44,23 @@ const Messages = () => {
setPreviewVisible(false); setPreviewVisible(false);
}; };
const handlePreview = (msg) => { const handlePreview = (msg) => {
if (msg.type !== 'photo') { switch (msg.type) {
return false; case 'photo':
setPreviewVisible(true);
setPreviewSrc(msg.data.uri);
return false;
case 'file':
window.open(msg.data.uri, '_blank', 'noopener,noreferrer');
return false;
default:
return false;
} }
setPreviewVisible(true);
setPreviewSrc(msg.data.uri);
}; };
const RenderText = memo(function renderText({str}) { const RenderText = memo(function renderText({ str }) {
const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation})/gmu).filter(s => s !== ''); const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== '');
// const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji}\uFE0F?|\b\d+\b)/gu); // const parts = str.split(/(https?:\/\/[^\s]+|\p{Emoji}\uFE0F?|\b\d+\b)/gu);
return ( return (
<> <>
@ -53,9 +68,8 @@ const Messages = () => {
// if (/\p{Emoji}\uFE0F?/u.test(part)) { // if (/\p{Emoji}\uFE0F?/u.test(part)) {
if (/\p{Emoji_Presentation}/u.test(part)) { if (/\p{Emoji_Presentation}/u.test(part)) {
const code = [...part].map((e) => e.codePointAt(0).toString(16)).join(`-`); const code = [...part].map((e) => e.codePointAt(0).toString(16)).join(`-`);
return <Emoji key={`${part}${index}${code}`} unified={code} size={24} emojiStyle={'apple'} />; return <Emoji key={`${part}${index}${code}`} unified={code} size={24} emojiStyle={'google'} />;
} else } else if (/https?:\/\/[\S]+/gi.test(part)) {
if (/https?:\/\/[\S]+/gi.test(part)) {
return ( return (
<a href={part} target='_blank' key={`${part}${index}`} rel='noreferrer'> <a href={part} target='_blank' key={`${part}${index}`} rel='noreferrer'>
{part} {part}
@ -78,25 +92,26 @@ const Messages = () => {
)); ));
return ( return (
<div> <div className='relative h-full overflow-y-auto flex'>
<Spin spinning={msgListLoading} tip={'正在读取...'} wrapperClassName='pt-8 '> <div className='relative overflow-y-auto block flex-1' ref={referance}>
{activeMessages.map((message, index) => ( <Spin spinning={msgListLoading} tip={'正在读取...'} wrapperClassName='pt-8 relative'>
// <Dropdown {activeMessages.map((message, index) => (
// key={message.id} // <Dropdown
// menu={{ // key={message.id}
// items: [{ label: '', key: 'reply', disabled: !['text'].includes(message.whatsapp_msg_type) }], // menu={{
// onClick: ({ key, domEvent }) => { // items: [{ label: '', key: 'reply', disabled: !['text'].includes(message.whatsapp_msg_type) }],
// domEvent.stopPropagation(); // onClick: ({ key, domEvent }) => {
// switch (key) { // domEvent.stopPropagation();
// case 'reply': // switch (key) {
// return setReferenceMsg(message); // case 'reply':
// return setReferenceMsg(message);
// default: // default:
// return; // return;
// } // }
// }, // },
// }} // }}
// trigger={['contextMenu']}> // trigger={['contextMenu']}>
<MessageBoxWithRef <MessageBoxWithRef
ref={messageRefs.current[index]} ref={messageRefs.current[index]}
key={message.id} key={message.id}
@ -105,23 +120,26 @@ const Messages = () => {
onReplyClick={() => setReferenceMsg(message)} onReplyClick={() => setReferenceMsg(message)}
onReplyMessageClick={() => scrollToMessage(message.reply.id)} onReplyMessageClick={() => scrollToMessage(message.reply.id)}
onOpen={() => handlePreview(message)} onOpen={() => handlePreview(message)}
onTitleClick={() => handlePreview(message)}
text={<RenderText str={message?.text || ''} />} text={<RenderText str={message?.text || ''} />}
{...(message.sender === 'me' {...(message.sender === 'me'
? { ? {
styles: { backgroundColor: '#ccd4ae' }, styles: { backgroundColor: '#ccd4ae' },
notchStyle: { fill: '#ccd4ae' }, notchStyle: { fill: '#ccd4ae' },
replyButton: ['text'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false, replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false,
className: 'whatsappme-container whitespace-pre-wrap', className: 'whatsappme-container whitespace-pre-wrap',
} }
: { : {
replyButton: ['text'].includes(message.whatsapp_msg_type) ? true : false, replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) ? true : false,
className: 'whitespace-pre-wrap',
})} })}
/> />
// </Dropdown> // </Dropdown>
))} ))}
</Spin>
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} /> <Image src={null} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
</Spin> </div>
<Button onClick={toBottom} shape={'circle'} className=' absolute bottom-1 right-4' icon={<DownOutlined />} />
</div> </div>
); );
}; };

@ -1,4 +1,3 @@
.ant-card .ant-card-head{ .ant-card .ant-card-head{
padding: 0 .5em .5em .5em; padding: 0 .5em .5em .5em;
min-height: unset; min-height: unset;
@ -18,8 +17,13 @@
background: linear-gradient(0deg,#00000014,#0000); background: linear-gradient(0deg,#00000014,#0000);
color: #00000073; color: #00000073;
} }
.chatwindow-wrapper .rce-mbox-text, .chatwindow-wrapper .referrer-msg { .chatwindow-wrapper .rce-mbox-text .emoji-text,
font-family: 'Twemoji Mozilla', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji', sans-serif; .chatwindow-wrapper .referrer-msg,
.chatwindow-wrapper .rce-mbox-reply-message
{
/* font-family: 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji', sans-serif; */
font-family: "Noto Color Emoji", 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'EmojiOne Color', 'Android Emoji', sans-serif;
font-weight: 500;
} }
.chatwindow-wrapper .rce-mbox-text a{ .chatwindow-wrapper .rce-mbox-text a{
color: #4f81a1; color: #4f81a1;
@ -60,6 +64,8 @@
.chatwindow-wrapper .rce-mbox .rce-mbox-reply { .chatwindow-wrapper .rce-mbox .rce-mbox-reply {
background-color: rgba(236, 236, 236, 0.7); background-color: rgba(236, 236, 236, 0.7);
} }
.chatwindow-wrapper .rce-mbox .epr-emoji-img{ .chatwindow-wrapper .rce-mbox .epr-emoji-img,
.chatwindow-wrapper .rce-mbox .epr-emoji-native
{
display: inline-block; display: inline-block;
} }

Loading…
Cancel
Save