React realizes the chat room history message and scroll bar sliding with the message

When we are developing im message chat, we will encounter two requirements:
1. When a new message is loaded, let the scroll bar automatically slide down
2. Scroll up to load the history record and keep the scroll bar at the original position

I showed you part of the code. I deleted part of the logic linked to the business. The code may be a little messy.
The main points to note are:

  • Use the scrollIntoView() method in the child component. This method will scroll the parent container of the element to make the element called scrollIntoView() visible to the user, which achieves the function of scrolling down as the message loads.
  useEffect(() => {
    
    
    if (shouldScroll) {
    
    
      $container.current.scrollIntoView()
    }
  }, [])
  • When loading message subcomponents in a loop, don’t use the index index for the key, but use the id, so that if two elements have the same key and satisfy the same element type, if the element attributes change, React only updates the component corresponding Property, it will also ensure that the scroll bar for loading history messages stays at the original position instead of scrolling to the end. (Please understand the React Key mechanism yourself for specific principles)

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)

Guess you like

Origin blog.csdn.net/zn740395858/article/details/113106801