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)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页