MessageBox.js
import React, { useRef, useEffect, memo } from 'react'
import PropTypes from 'prop-types'
import { Spin } from 'antd'
import { isEmpty } from 'lodash'
import * as api from '../../api'
import MessageItemBox from './MessageItemBox'
import '../index.scss'
const MessageBox = ({
isEnd,
queryChatRecord,
loading,
action = {},
socket = {},
focus,
sessionList = {},
customerInfo = {},
messages = {},
}) => {
const $containerEl = useRef()
let isFetching = false // 判断是否是拉取数据操作
// 上滑滚动加载
const handleScroll = async e => {
const { scrollHeight, clientHeight, scrollTop } = $containerEl.current || {}
if (scrollTop + clientHeight === scrollHeight && sessionList[focus]?.messages?.hasNew) {
// 监听当滑动到底去掉新消息提醒(业务相关,可忽略)
action.clearMessageStatus({ sessionId: focus })
}
if (isEnd) {
return
}
if ($containerEl.current && e.target !== $containerEl.current) {
return
}
if (isFetching) {
return
}
const $div = e.target
if ($div.scrollTop === 0 && $div.scrollHeight > $div.clientHeight && !loading) {
isFetching = true
queryChatRecord() // 拉取历史消息
isFetching = false
}
}
/**
*
* 按照消息发送时间排序
* @param {*} session1
* @param {*} session2
* @return {*}
*/
const sort = (session1, session2) => {
return (session1.createTime) > (session2.createTime) ? 1 : -1
}
// 滚动到底部
const handleScrollBottom = () => {
const { scrollHeight, clientHeight } = $containerEl.current
$containerEl.current.scrollTop = scrollHeight - clientHeight
// 清除新消息提醒
action.clearMessageStatus({ sessionId: focus })
}
const renderMessage = (item, index) => {
let shouldScroll = true
const isSelf = item.from?.uid === customerInfo.uid
// 【重点】
if ($containerEl.current) {
const { scrollHeight, clientHeight, scrollTop } = $containerEl.current
shouldScroll = isSelf ||
scrollHeight === clientHeight ||
scrollTop === 0 ||
scrollTop > scrollHeight - clientHeight * 2
}
return (
<MessageItemBox
key={item.imMsgId} //【重点】key必须使用数组内的唯一值,而不能使用index
content={item.content}
type={item.type}
direction={item.direction || 'right'}
shouldScroll={shouldScroll}
avatar={item.from?.avatar}
username={item.from?.name}
createTime={item.createTime}
loading={item.loading}
success={item.success}
sendContent={item.sendContent}
focus={focus}
imMsgId={item.imMsgId}
socket={socket}
action={action}
/>
)
}
return (
<>
<div
styleName='session-content-dialog'
ref={$containerEl}
onScroll={handleScroll}
>
{
!isEnd && loading && <div className='flex-column' style={{ width: '100%' }}> <Spin spinning={loading} /></div>
}
{!isEmpty(messages) && messagesInfo.sort(sort).map((item, index) =>
renderMessage(item, index)
)}
{isEmpty(messages) && !loading ? (
<div style={{ textAlign: 'center', color: '#969696', marginTop: 30 }}>无记录</div>
) : (
''
)}
</div>
{ messages?.hasNew && (
<div className='flex-row-reverse' style={{ width: '100%' }} onClick={handleScrollBottom}>
<div
style={{
backgroundColor: '#fff',
textAlign: 'center',
color: '#1890ff',
display: 'inline-block',
zIndex: 10,
width: 100,
padding: 5,
borderRadius: 8,
marginTop: '-34px',
cursor: 'pointer',
}}
>你有新消息
</div>
</div>
)
}
</>
)
}
MessageBox.propTypes = {
isEnd: PropTypes.bool,
loading: PropTypes.bool,
queryChatRecord: PropTypes.func,
action: PropTypes.any,
socket: PropTypes.any,
messages: PropTypes.object,
focus: PropTypes.string,
sessionList: PropTypes.object,
customerInfo: PropTypes.object,
}
export default memo(MessageBox)
MessageItemBox.js
import React, { useEffect, useRef, useState, memo, lazy, Suspense } from 'react'
import PropTypes from 'prop-types'
import { Icon, message } from 'antd'
import moment from 'moment'
import { post } from 'utils/request'
import { emojiData } from '../../config'
import '../index.scss'
const MediaMessage = lazy(() => import('./MediaMessage'))
const validKnowledge = payload => post('/im/imMessageService/validKnowledge', payload)
/** 客服相关 */
// 客服状态列表
const MessageItemBox = ({
msgSource = 1,
createTime,
content = {},
direction,
avatar,
type,
shouldScroll,
loading,
success,
username,
sendContent = {},
focus,
imMsgId,
action,
socket,
}) => {
const $container = useRef()
// const action = useAction()
// const socket = useSocket()
const [curLoading, setCurLoading] = useState(loading)
const [visible, setVisible] = useState(false)
useEffect(() => {
// 【重点】判断是否需要滚动,滚动条自动向下滑动
if (shouldScroll) {
$container.current.scrollIntoView()
}
}, [])
useEffect(() => {
setCurLoading(loading)
}, [loading])
const handleMedia = () => {
setVisible(true)
}
const getContent = () => {
switch (type) {
...
default: {
if (!content.msg) return ''
const res = renderText(content.msg)
return <>
<div styleName='dialogue-arrow' />
{content.msg}
</>
}
}
}
/**
* 重发消息
*/
const handleReSend = async () => {
setCurLoading(true)
try {
const res = await socket.send(sendContent)
action.updateSessionMessage(focus, imMsgId, sendContent, res)
} catch (error) {
action.updateSessionMessage(focus, imMsgId, sendContent, { sendSuccess: false })
}
setCurLoading(false)
}
return (
<div style={{ textAlign: 'center', marginBottom: 10 }} ref={$container}>
<div className='flex-column' style={{ alignItems: direction === 'left' ? 'flex-start' : 'flex-end' }}>
<div className='mb8'>
{ `${username}(${moment(createTime).format('YYYY-MM-DD HH:mm:ss')})`}
</div>
<div
style={{ display: 'flex', flexDirection: direction === 'left' ? 'row' : 'row-reverse', alignItems: 'center' }}
>
<div>
<span styleName='dialogue-avatar'>
<img src={avatar + '?imageView2/1/w/40/h/40'} />
</span>
</div>
<div styleName={`dialogue-popover-${direction}`}>{getContent()}</div>
{curLoading && <Icon type='loading' />}
{!success && !curLoading &&
<div onClick={handleReSend}><Icon type='exclamation' style={{ color: 'red' }} /></div>
}
</div>
</div>
{visible &&
<Suspense>
<MediaMessage visible={visible} type={type} onCancel={() => setVisible(false)} src={content.url} />
</Suspense>
}
</div>
)
}
MessageItemBox.propTypes = {
msgSource: PropTypes.number,
createTime: PropTypes.number,
type: PropTypes.number,
content: PropTypes.object,
shouldScroll: PropTypes.bool,
avatar: PropTypes.string,
direction: PropTypes.string,
loading: PropTypes.bool,
success: PropTypes.bool,
sendContent: PropTypes.object,
imMsgId: PropTypes.string,
focus: PropTypes.string,
username: PropTypes.string,
action: PropTypes.any,
socket: PropTypes.any,
}
export default memo(MessageItemBox)