react实现聊天室历史消息及滚动条随着消息滑动功能

当我们在开发im消息聊天的时候,都会遇到两种需求
1、当加载了新消息后让滚动条自动向下滑动
2、向上滚动加载历史记录滚动条保持在原来位置上

给大家展示了部分代码,我删除了和业务挂钩的部分逻辑,可能代码会有一点点的乱。
主要要注意的是:

  • 在子组件中使用scrollIntoView()方法,这个方法会滚动元素的父容器,使被调用scrollIntoView()的元素对用户可见,也就达到了滚动条随着消息加载向下滑动的功能。
  useEffect(() => {
    
    
    if (shouldScroll) {
    
    
      $container.current.scrollIntoView()
    }
  }, [])
  • 循环加载消息子组件的时候,key不要用index索引,而是要用id,这样如果两个元素是相同的key,且满足元素类型相同, 若元素属性有所变化,则React只更新组件对应的属性,也就会保证加载历史消息滚动条保持在原来位置,而不是直接滚动到底。(具体原理请自行了解React Key机制)

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)

猜你喜欢

转载自blog.csdn.net/zn740395858/article/details/113106801