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

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

@ -48,6 +48,7 @@ const mediaMsg = {
...(msg.context
? {
reply: {
id: msg.message_origin.id,
message: msg.message_origin.text,
title: msg.message_origin.senderName || 'Reference',
titleColor: msg.message_origin?.senderName !== 'me' ? '#a791ff' : '#128c7e',
@ -76,10 +77,11 @@ export const sentMsgTypeMapped = {
...(msg.context
? {
reply: {
id: msg.message_origin.id,
message: msg.message_origin.text,
title: msg.message_origin.senderName || 'Reference',
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,
text: msg.image.caption,
data: { id: msg.wamid, uri: msg.image.link, width: '100%', height: 200, alt: msg.image.caption, },
originText: msg.image?.caption || '',
onOpen: () => {
console.log('Open image', msg.image.link);
},
@ -225,6 +228,9 @@ export const whatsappMsgTypeMapped = {
id: msg.wamid,
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: {
type: 'video',
@ -241,7 +247,7 @@ export const whatsappMsgTypeMapped = {
},
}),
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: {
@ -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: {
type: 'text',
data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '', }),
},
document: {
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',
// contact: 'contact',

@ -2,7 +2,7 @@ import { create } from 'zustand';
import { RealTimeAPI } from '@/lib/realTimeAPI';
import { olog, isEmpty } from '@/utils/utils';
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 { WS_URL } from '@/config';
@ -168,7 +168,7 @@ const conversationSlice = (set, get) => ({
},
setCurrentConversation: (conversation) => {
// 清空未读
const { conversationsList, totalNotify } = get();
const { conversationsList } = get();
const targetId = conversation.sn;
const targetIndex = conversationsList.findIndex((ele) => String(ele.sn) === String(targetId));
targetIndex !== -1
@ -178,7 +178,12 @@ const conversationSlice = (set, get) => ({
})
: 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 }),
receivedMessageList: (conversationid, msgList) =>
set((state) => ({
msgListLoading: false,
// msgListLoading: false,
activeConversations: { ...state.activeConversations, [String(conversationid)]: msgList },
})),
updateMessageItem: (message) => {
@ -280,7 +285,7 @@ export const useConversationStore = create(
// side effects
fetchInitialData: async (userId) => {
const { addToConversationList, setTemplates, setInitial } = get();
const { addToConversationList, setTemplates, setInitial, receivedMessageList } = get();
const conversationsList = await fetchConversationsList({ opisn: userId });
addToConversationList(conversationsList);
@ -289,6 +294,11 @@ export const useConversationStore = create(
setTemplates(templates);
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={collapsedRight ? <MenuFoldOutlined /> : <MenuUnfoldOutlined />} onClick={() => setCollapsedRight(!collapsedRight)} className=' rounded-none rounded-r' />
</Header>
<Content className="flex-grow bg-whatsapp-bg" >
<div className='h-full overflow-y-auto'>
<Content className="flex-grow bg-whatsapp-bg relative" >
<Messages />
</div>
</Content>
<Footer className='ant-layout-sider-light p-0'>
<InputComposer />

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

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

@ -97,8 +97,9 @@ const InputComposer = () => {
{referenceMsg.id && (
<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'>
<span className=' text-primary pr-1 italic'>{referenceMsg.senderName}</span>
{referenceMsg.originText}
<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'>{referenceMsg.originText}</span>
</div>
<Button type='text' title='取消引用' className=' rounded-none text-slate-500' icon={<CloseCircleOutlined />} size={'middle'} onClick={() => setReferenceMsg({})} />
</Flex>
@ -134,7 +135,7 @@ const InputComposer = () => {
/>
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'>
<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} />
{/* <InputImageUpload key={'addNewPic'} disabled={!textabled} invokeSendMessage={invokeSendMessage} /> */}
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />

@ -1,13 +1,13 @@
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 { MessageBox } from 'react-chat-elements';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
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 setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
const msgListLoading = useConversationStore(useShallow((state) => state.msgListLoading));
@ -17,19 +17,26 @@ const Messages = () => {
const scrollToMessage = (id, index) => {
const _i = index || activeMessages.findIndex((msg) => msg.id === id);
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(() => {
if (activeMessages.length > 0) {
// toBottom();
scrollToMessage(null, activeMessages.length - 1);
}
}, [activeMessages]);
const messageRefs = useRef([]);
messageRefs.current = activeMessages.map((_, i) => messageRefs.current[i] ?? createRef());
const [previewVisible, setPreviewVisible] = useState(false);
const [previewSrc, setPreviewSrc] = useState();
const onPreviewClose = () => {
@ -37,15 +44,23 @@ const Messages = () => {
setPreviewVisible(false);
};
const handlePreview = (msg) => {
if (msg.type !== 'photo') {
return false;
}
switch (msg.type) {
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;
}
};
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);
return (
<>
@ -53,9 +68,8 @@ const Messages = () => {
// if (/\p{Emoji}\uFE0F?/u.test(part)) {
if (/\p{Emoji_Presentation}/u.test(part)) {
const code = [...part].map((e) => e.codePointAt(0).toString(16)).join(`-`);
return <Emoji key={`${part}${index}${code}`} unified={code} size={24} emojiStyle={'apple'} />;
} else
if (/https?:\/\/[\S]+/gi.test(part)) {
return <Emoji key={`${part}${index}${code}`} unified={code} size={24} emojiStyle={'google'} />;
} else if (/https?:\/\/[\S]+/gi.test(part)) {
return (
<a href={part} target='_blank' key={`${part}${index}`} rel='noreferrer'>
{part}
@ -78,8 +92,9 @@ const Messages = () => {
));
return (
<div>
<Spin spinning={msgListLoading} tip={'正在读取...'} wrapperClassName='pt-8 '>
<div className='relative h-full overflow-y-auto flex'>
<div className='relative overflow-y-auto block flex-1' ref={referance}>
<Spin spinning={msgListLoading} tip={'正在读取...'} wrapperClassName='pt-8 relative'>
{activeMessages.map((message, index) => (
// <Dropdown
// key={message.id}
@ -105,23 +120,26 @@ const Messages = () => {
onReplyClick={() => setReferenceMsg(message)}
onReplyMessageClick={() => scrollToMessage(message.reply.id)}
onOpen={() => handlePreview(message)}
onTitleClick={() => handlePreview(message)}
text={<RenderText str={message?.text || ''} />}
{...(message.sender === 'me'
? {
styles: { backgroundColor: '#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',
}
: {
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>
))}
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
</Spin>
<Image src={null} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
</div>
<Button onClick={toBottom} shape={'circle'} className=' absolute bottom-1 right-4' icon={<DownOutlined />} />
</div>
);
};

@ -1,4 +1,3 @@
.ant-card .ant-card-head{
padding: 0 .5em .5em .5em;
min-height: unset;
@ -18,8 +17,13 @@
background: linear-gradient(0deg,#00000014,#0000);
color: #00000073;
}
.chatwindow-wrapper .rce-mbox-text, .chatwindow-wrapper .referrer-msg {
font-family: 'Twemoji Mozilla', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji', sans-serif;
.chatwindow-wrapper .rce-mbox-text .emoji-text,
.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{
color: #4f81a1;
@ -60,6 +64,8 @@
.chatwindow-wrapper .rce-mbox .rce-mbox-reply {
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;
}

Loading…
Cancel
Save