style: 移动端: 邮件阅读/回复; 消息筛选等

dev/email
Lei OT 9 months ago
parent b2884ce0a0
commit cae2a353d2

@ -3,6 +3,7 @@ import {} from 'antd';
import Modal from '@dckj/react-better-modal';
import '@dckj/react-better-modal/dist/index.css';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial = {}, title, footer=null, ...props }) => {
// const [open, setOpen] = useState(false);
@ -30,9 +31,11 @@ const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial
}
setOpen(false);
}
function onStageChange({ state }) {
function onStageChange({ state, target }) {
// console.log(state);
}
const [mobile] = useStyleStore((state) => [state.mobile]);
return (
<Modal
visible={open}
@ -48,10 +51,10 @@ const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial
contentClassName='!p-2'
footerClassName='!p-2'
zIndex={2}
initialWidth={initial.width || 680}
initialHeight={initial.height || 600}
initialTop={initial.top || 74}
initialLeft={initial.left || window.innerWidth - 700}
initialWidth={(mobile ? window.innerWidth : (initial.width || 680))} // window.innerWidth < 680
initialHeight={(mobile ? window.innerHeight : (initial.height || 600))} // window.innerHeight < 700
initialTop={mobile ? 0 : (initial.top || 74)}
initialLeft={mobile ? 0 : (initial.left || (window.innerWidth - 700))}
title={title}
minimizeButton={<></>}
onMove={onHandleMove}
@ -59,8 +62,9 @@ const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial
onCancel={onHandleCancel}
// onOk={onHandleOk}
onStageChange={onStageChange}
footer={footer}>
<>{children}</>
footer={footer}
{...(mobile ? { maximizeButton: <></> } : {})}>
<>{children}</>
</Modal>
);
};

@ -0,0 +1,10 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useStyleStore = create(
devtools((set, get) => ({
mobile: false,
setMobile: (mobile) => set({ mobile }),
}))
);
export default useStyleStore;

@ -109,6 +109,18 @@
background: unset;
/* box-shadow: none; */
}
.chatwindow-wrapper .rce-mbox{
margin-left: 10px;
margin-right: 5px;
}
.chatwindow-wrapper .rce-mbox.rce-mbox-right{
margin-left: 5px;
margin-right: 10px;
}
.chatwindow-wrapper .rce-mbox.rce-mbox--clear-notch{
margin-left: 5px;
margin-right: 5px;
}
.chatwindow-wrapper .bg-transparent .rce-mbox-left-notch,
.chatwindow-wrapper .bg-transparent .rce-mbox-right-notch
{

@ -10,6 +10,7 @@ import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore';
import ChannelLogo from './ChannelLogo';
import { DeliverIcon, ReadIcon, SentIcon } from '@/components/Icons';
import useStyleStore from '@/stores/StyleStore';
const TagColorStyle = (tag) => {
const color = stringToColour(tag);
@ -91,9 +92,11 @@ const EditChatMemoForm = ({onSubmit,...props}) => {
};
const ChatListItem = (({mobile, item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation, setNewChatModalVisible,setEditingChat,...props}) => {
const routerReplace = mobile === undefined ? true : false; // : true;
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation, setNewChatModalVisible,setEditingChat,...props}) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const routerReplace = mobile === false ? true : false; // : true;
const routePrefix = mobile === false ? `/order/chat` : `/m/chat`;
const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams();

@ -41,7 +41,7 @@ const EmailDetail = ({ open, setOpen, emailDetail, ...props }) => {
return (
<>
<DnDModal open={open} setOpen={setOpen} title={emailOrigin.subject} initial={{ top: 74, width: 680, height: 600 }} onMove={onHandleMove} onResize={onHandleResize}>
<DnDModal open={open} setOpen={setOpen} title={emailOrigin.subject} initial={{ top: 74 }} onMove={onHandleMove} onResize={onHandleResize}>
{/* email toolbar */}
<div className='email-container flex flex-col gap-2 *:p-2 *:rounded-sm *:border-b *:border-gray-200 *:shadow-1md'>
{/* <div className='flex items-center justify-start '>
@ -50,14 +50,17 @@ const EmailDetail = ({ open, setOpen, emailDetail, ...props }) => {
</Button>
</div> */}
<div className=' font-bold'>{emailOrigin.subject}</div>
<div>
<div className='flex justify-between'>
<div className={['flex justify-between', window.innerWidth < 600 ? 'flex-row' : 'flex-row'].join(' ')}>
<div className='flex gap-2 mb-2 items-center'>
<Avatar className='' style={TagColorStyle(emailOrigin.fromEmail)}>
{(emailOrigin.fromName || '').substring(0, 1)}
</Avatar>
<span className=' font-bold text-base'>{emailOrigin.fromName}</span>
<span className='text-neutral-600'>&lt;{emailOrigin.fromEmail}&gt;</span>
<div className=' flex flex-col'>
<span className=' font-bold text-base'>{emailOrigin.fromName}</span>
<span className='text-neutral-500'>{emailOrigin.fromEmail}</span>
</div>
</div>
<div className='flex flex-col justify-start gap-1 items-end'>
<div className='flex items-center '>

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { App, Button, Popover, Tabs, List, Image, Avatar, Card } from 'antd';
import { App, Button, Popover, Tabs, List, Image, Avatar, Card, Flex } from 'antd';
import { FileSearchOutlined, LoadingOutlined } from '@ant-design/icons';
import { InboxIcon, SendPlaneFillIcon } from '@/components/Icons';
import { groupBy, stringToColour } from '@/utils/commons';
@ -8,6 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
import EmailDetail from './EmailDetail';
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';
import DnDModal from '@/components/DnDModal';
import useStyleStore from '@/stores/StyleStore';
const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 10;
@ -25,6 +26,8 @@ const getVideoName = (vUrl) => {
* 消息记录筛选----------------------------------------------------------------------------------------------------
*/
const MessageListFilter = ({ ...props }) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const activeMessages = useConversationStore(
useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn] : []))
);
@ -102,7 +105,7 @@ const MessageListFilter = ({ ...props }) => {
renderItem={(date) => (
<List.Item>
<Card size='small' title={date} key={date} className='mb-2'>
<div className='grid grid-cols-5 gap-2 '>
<div className={['grid gap-2 ', mobile === false ? 'grid-cols-5' : 'grid-cols-3'].join(' ')}>
{byDate[date].map((img) => (
<Image className='border object-cover' key={img.data.id} width={100} height={100} src={img.data.uri} />
))}
@ -136,28 +139,24 @@ const MessageListFilter = ({ ...props }) => {
loadMore={loadMore}
loading={loading}
renderItem={(item) => (
<List.Item
actions={[
<Button key='copyv' onClick={() => handleCopyClick(item.data.videoURL)} type='link' size='small'>
复制🔗
</Button>,
item.localDate,
]}
className='items-center'>
<List.Item>
<List.Item.Meta
avatar={
<Avatar size='small' style={CalColorStyle(item.sender)}>
{item.senderName}
</Avatar>
}
title={
<span onClick={() => handleOpenVideoPlay(item?.data.videoURL)}>
{getVideoName(item?.data.videoURL)}
</span>
title={<span onClick={() => handleOpenVideoPlay(item?.data.videoURL)}>{getVideoName(item?.data.videoURL)}</span>}
description={
<Flex>
<div className='flex-auto'>{item.localDate}</div>
<Button key='copyv' onClick={() => handleCopyClick(item.data.videoURL)} type='link' size='small'>
复制🔗
</Button>
</Flex>
}
description={item.text}
/>
{/* {item.text} */}
{item.text}
</List.Item>
)}
/>
@ -209,12 +208,8 @@ const MessageListFilter = ({ ...props }) => {
loadMore={loadMore}
loading={loading}
renderItem={(item) => (
<List.Item actions={[
<Button key='copyv' onClick={() => handleCopyClick(item.data.uri)} type='link' size='small'>
复制🔗
</Button>,item.localDate]}>
<List.Item>
<List.Item.Meta
// avatar={<Avatar src={`https://api.dicebear.com/7.x/miniavs/svg?seed=${index}`} />}
avatar={
<Avatar size='small' style={CalColorStyle(item.sender)}>
{item.senderName}
@ -222,12 +217,19 @@ const MessageListFilter = ({ ...props }) => {
}
title={
<a href={item.data.uri} target='_blank' rel='noreferrer'>
{item.title} 🔗
{item.title}
</a>
}
description={item.text}
description={
<Flex>
<div className='flex-auto'>{item.localDate}</div>
<Button key='copyv' onClick={() => handleCopyClick(item.data.videoURL)} type='link' size='small'>
复制🔗
</Button>
</Flex>
}
/>
{/* {item.text} */}
{item.text}
</List.Item>
)}
/>
@ -256,7 +258,7 @@ const MessageListFilter = ({ ...props }) => {
loading={loading}
renderItem={({ emailOrigin, ...item }) => (
<List.Item
actions={[item.localDate]}
// actions={[item.localDate]}
className='cursor-pointer'
onClick={() => {
onOpenEmail({ emailOrigin, ...item });
@ -270,7 +272,12 @@ const MessageListFilter = ({ ...props }) => {
// </Avatar>
}
title={emailOrigin.subject}
description={`To: ${emailOrigin.toEmail}`}
// description={`To: ${emailOrigin.toEmail}`}
description={
<Flex justify={'space-between'} className='max-w-full overflow-hidden' >
<div className='flex-auto line-clamp-1 break-all pr-2'>{`To: ${emailOrigin.toEmail}`}</div>
<div className=' basis-32 flex-grow-0 flex-shrink-0'>{item.localDate}</div>
</Flex>}
/>
{emailOrigin.abstract}
</List.Item>
@ -286,10 +293,10 @@ const MessageListFilter = ({ ...props }) => {
return (
<>
<Popover
// overlayClassName={[mobile === undefined ? 'w-3/5' : 'w-full max-h-full'].join(' ')}
// destroyTooltipOnHide
placement='bottom'
overlayClassName='w-2/5 max-w-2/5 max-h-[70%]'
overlayClassName={[mobile === false ? 'w-2/5' : 'w-full max-h-full', 'max-h-[70%]'].join(' ')}
// 'min-w-2/5 max-w-2/5 max-h-[70%]'
trigger={'click'}
open={openPopup}
onOpenChange={setOpenPopup}

@ -17,7 +17,7 @@ const fetchBindOrder = async (params) => {
// return errcode !== 0 ? {} : result;
};
export const ConversationBindFormModal = ({ mobile, currentConversationID, userId, onBoundSuccess }) => {
export const ConversationBindFormModal = ({ currentConversationID, userId, onBoundSuccess }) => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false); // bind loading
@ -119,7 +119,7 @@ export const ConversationBindFormModal = ({ mobile, currentConversationID, userI
现在关联
</Button>}
<Modal
width={mobile === undefined ? '95%' : '100%'}
width={window.innerWidth < 700 ? '95%' : '100%'}
open={open}
title={'关联订单'}
footer={false}

@ -12,6 +12,7 @@ import { useVisibilityState } from '@/hooks/useVisibilityState';
import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore'
import ChatListItem from './Components/ChatListItem';
import ChatListFilter from './Components/ChatListFilter';
import useStyleStore from '@/stores/StyleStore';
const { Option, OptGroup } = Select;
const { useToken } = theme;
@ -19,7 +20,7 @@ const { useToken } = theme;
/**
* []
*/
const Conversations = ({ mobile }) => {
const Conversations = () => {
const { token } = useToken();
const contentStyle = {
backgroundColor: token.colorBgElevated,
@ -29,9 +30,9 @@ const Conversations = ({ mobile }) => {
const menuStyle = {
boxShadow: 'none',
};
const routerReplace = mobile === undefined ? true : false; // : true;
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
const [mobile] = useStyleStore((state) => [state.mobile]);
const routerReplace = mobile === false ? true : false; // : true;
const routePrefix = mobile === false ? `/order/chat` : `/m/chat`;
const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams();
@ -277,9 +278,7 @@ const Conversations = ({ mobile }) => {
type='text'
/>
</Tooltip>
{mobile === undefined ? (
''
) : (
{mobile && (
<AudioTwoTone
onClick={() => {
navigate(`/callcenter/call`);
@ -296,7 +295,10 @@ const Conversations = ({ mobile }) => {
) : null}
{dataSource.map((item) => (
<ChatListItem key={item.sn} {...{mobile, item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation,setNewChatModalVisible,setEditingChat}} />
<ChatListItem
key={item.sn}
{...{ item, refreshConversationList, setListUpdateFlag, onSwitchConversation, tabSelectedConversation, setNewChatModalVisible, setEditingChat }}
/>
))}
{dataSource.length === 0 && <Empty description={'无数据'} />}
</div>

@ -103,7 +103,7 @@ export const ConversationItemFormModal = ({ open, onCreate, onCancel, initialVal
return (
<Modal
open={open}
open={open} style={{ top: 20 }}
title={initialValues.is_new ? '新建会话' : '编辑会话'}
okText='确认'
// cancelText='Cancel'

@ -3,6 +3,7 @@ import { Button, Form, Input, Flex, Checkbox, Switch, Mentions, Popover, Popconf
import Modal from '@dckj/react-better-modal';
import '@dckj/react-better-modal/dist/index.css';
import DnDModal from '@/components/DndModal';
import useStyleStore from '@/stores/StyleStore';
import LexicalEditor from '@/components/LexicalEditor';
@ -37,9 +38,11 @@ const getAbstract = (longtext) => {
};
const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, quote = {}, initial = {}, action = 'reply', ...props }) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const [form] = Form.useForm();
const [isRichText, setIsRichText] = useState(true);
const [isRichText, setIsRichText] = useState((mobile === false));
const [htmlContent, setHtmlContent] = useState('');
const [textContent, setTextContent] = useState('');
@ -154,7 +157,7 @@ const EmailEditorPopup = ({ open, setOpen, fromEmail, reference, quote = {}, ini
<DnDModal
open={open}
setOpen={setOpen}
initial={{ top: isEmpty(reference) ? 20 : 74, width: 680, height: 600 }}
initial={{ top: isEmpty(reference) ? 20 : 74 }}
onCancel={() => {
form.resetFields();
}}

@ -1,9 +1,11 @@
import { useState } from 'react';
import { Popover, Button } from 'antd';
import EmojiPicker from 'emoji-picker-react';
import useStyleStore from '@/stores/StyleStore';
const InputTemplate = ({ mobile, disabled = false, inputEmoji }) => {
const InputTemplate = ({ disabled = false, inputEmoji }) => {
const [openPopup, setOpenPopup] = useState(false);
const [mobile] = useStyleStore((state) => [state.mobile]);
const handlePickEmoji = (emojiData) => {
inputEmoji(emojiData.emoji);
@ -13,7 +15,7 @@ const InputTemplate = ({ mobile, disabled = false, inputEmoji }) => {
<>
<Popover
overlayClassName='p-0'
placement={mobile === undefined ? 'right' : 'top'}
placement={mobile === false ? 'right' : 'top'}
overlayInnerStyle={{ padding: 0, borderRadius: '8px' }}
forceRender={true}
content={<EmojiPicker skinTonesDisabled={true} emojiStyle='native' onEmojiClick={handlePickEmoji} className='chatwindow-wrapper' />}

@ -27,6 +27,7 @@ import { OSS_URL as aliOSSHost } from '@/config';
import { postUploadFileItem } from '@/actions/CommonActions';
import ExpireTimeClock from '../ExpireTimeClock';
import dayjs from 'dayjs';
import useStyleStore from '@/stores/StyleStore';
const ButtonStyleClsMapped =
{
@ -34,7 +35,9 @@ const ButtonStyleClsMapped =
'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 InputComposer = ({ isWABA, channel }) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const userId = useAuthStore((state) => state.loginUser.userId);
const websocket = useConversationStore((state) => state.websocket);
const websocketOpened = useConversationStore((state) => state.websocketOpened);
@ -61,7 +64,7 @@ const InputComposer = ({ mobile, isWABA, channel }) => {
// console.groupEnd();
const textPlaceHolder = !textabled
? ''
: mobile === undefined
: mobile === false
? 'Enter 发送, Shift+Enter 换行'
: 'Enter 换行';
@ -279,7 +282,7 @@ const InputComposer = ({ mobile, isWABA, channel }) => {
? '请先选择会话'
: !textabled0 && isWABA
? '会话已超24h不活跃. 请发送打招呼消息激活对话💬.'
: mobile === undefined
: mobile === false
? 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'
: 'Enter 换行, 点击 Send 发送'
}
@ -289,7 +292,7 @@ const InputComposer = ({ mobile, isWABA, channel }) => {
onChange={(e) => setTextContent(e.target.value)}
className='rounded-b-none emoji'
onPressEnter={(e) => {
if (!e.shiftKey && mobile === undefined) {
if (!e.shiftKey && mobile === false) {
e.preventDefault();
if (textabled && !pastedUploading) handleSendText();
}
@ -298,8 +301,8 @@ const InputComposer = ({ mobile, isWABA, channel }) => {
/>
<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 }} />
{isWABA && <InputTemplate key='templates' disabled={!talkabled} invokeSendMessage={invokeSendMessage} />}
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} />

@ -6,6 +6,7 @@ import useConversationStore from '@/stores/ConversationStore';
import { cloneDeep, getNestedValue, objectMapper, removeFormattingChars, sortArrayByOrder } from '@/utils/commons';
import { replaceTemplateString } from '@/channel/whatsappUtils';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
const splitTemplate = (template) => {
const placeholders = template.match(/{{(.*?)}}/g) || [];
@ -21,7 +22,9 @@ const splitTemplate = (template) => {
}, []);
return obj;
};
const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
const [mobile] = useStyleStore((state) => [state.mobile]);
const searchInputRef = useRef(null);
const { notification } = App.useApp();
const loginUser = useAuthStore((state) => state.loginUser);
@ -185,7 +188,7 @@ const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
return (
<>
<Popover
overlayClassName={[mobile === undefined ? 'w-3/5' : 'w-full max-h-full'].join(' ')}
overlayClassName={[mobile === false ? 'w-3/5' : 'w-full max-h-full'].join(' ')}
fresh
forceRender
destroyTooltipOnHide={true}

@ -98,7 +98,7 @@ const EmailEditor = ({ mobile, open, setOpen, fromEmail, reference, ...props })
okButtonProps={{ className: 'bg-indigo-500 shadow shadow-indigo-300 hover:!bg-indigo-400 active:bg-indigo-400 focus:bg-indigo-400' }}
cancelButtonProps={{ className: 'hover:!text-indigo-500 hover:!border-indigo-400 active:border-indigo-400 focus:border-indigo-400' }}
style={{ bottom: 0, left: '18%' }}
width={mobile === undefined ? '800px' : '100%'}
width={mobile === false ? '800px' : '100%'}
zIndex={2}
// footer={false}
onCancel={() => setOpen(false)}

@ -193,7 +193,7 @@ const MessagesWrapper = ({ updateRead = true, forceGetMessages }) => {
onCancel={() => setNewChatModalVisible(false)}
/>
{/* <EmailEditor open={openEmailEditor} setOpen={setOpenEmailEditor} reference={ReferEmailMsg} setRefernce={setReferEmailMsg} {...{ fromEmail, }} key={'email-editor-reply'} /> */}
<EmailEditorPopup open={openEmailEditor} setOpen={setOpenEmailEditor} fromEmail={fromEmail} quote={ReferEmailMsg} key={`email-editor-reply-top-popup_${ReferEmailMsg.id}`}/>
<EmailEditorPopup open={openEmailEditor} setOpen={setOpenEmailEditor} fromEmail={fromEmail} quote={ReferEmailMsg} key={`email-editor-reply-top-popup_${ReferEmailMsg.id}`} />
<EmailDetail open={openEmailDetail} setOpen={setOpenEmailDetail} emailDetail={emailDetail} key={`email-detail-${emailDetail.id}`} />
</>
);

@ -9,6 +9,7 @@ import { NavLink, Outlet, Link } from 'react-router-dom';
import ReloadPrompt from './ReloadPrompt';
import ClearCache from './ClearCache';
import PageSpy from './PageSpy';
import useStyleStore from '@/stores/StyleStore';
import { BUILD_VERSION } from '@/config';
@ -23,7 +24,11 @@ function MobileApp() {
token: { colorBgContainer },
} = theme.useToken();
const [setMobile] = useStyleStore((state) => [state.setMobile]);
useEffect(() => {
setMobile(true);
const handleLoad = () => {
const isPWAInstalled = window.matchMedia('(display-mode: window-controls-overlay)').matches || window.matchMedia('(display-mode: standalone)').matches;
const isStandalone = navigator.standalone || window.navigator.standalone;

@ -21,7 +21,7 @@ function Chat() {
<MessagesWrapper />
</Content>
<Footer className='ant-layout-sider-light p-0'>
<InputComposer mobile />
<InputComposer />
</Footer>
</Layout>
</>

@ -4,7 +4,7 @@ function Conversation() {
return (
<div className='chatwindow-wrapper'>
<ConversationsList mobile={true} />
<ConversationsList />
</div>
)
}

Loading…
Cancel
Save