从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十三) 优化聊天页面

1、 ChatBox组件

目前我们收到新的消息 不会滚动到视野当中

修改chatbox组件

// 最后一条消息的ref

const lastMessageRef = useRef(null)

// 滚动到最新消息

useEffect(()=>{

if(lastMessageRef.current && messages) {

lastMessageRef.current.scrollIntoView({behavior: 'smooth'})

}

})

javascript 复制代码
import {useEffect,useRef} from "react"
import { useChatStore } from "../store/useChatStore"
import { useAuthStore } from "../store/useAuthStore"
import {formatMessageTime} from "@/lib/util"
import ChatHeader from "./ChatHeader"
import MessageInput from "./MessageInput"

const ChatBox = () => {
  const {messages, getMessages, isMessagesLoading, selectedUser, subscribeToMessages, unsubscribeFromMessages} = useChatStore()
  const {authUser} = useAuthStore()
  // 最后一条消息的ref
  const lastMessageRef = useRef(null)
  useEffect(()=>{
    getMessages(selectedUser._id)

    // 开始订阅消息
    subscribeToMessages()

    return () => unsubscribeFromMessages()
  },[selectedUser._id, getMessages,subscribeToMessages, unsubscribeFromMessages])

  // 滚动到最新消息
  useEffect(()=>{
    if(lastMessageRef.current && messages) {
      lastMessageRef.current.scrollIntoView({behavior: 'smooth'})
    }
  })
  if(isMessagesLoading) return <div>Loading...</div>
  return (
    <div className="flex-1 flex flex-col overflow-auto">
      {/* 聊天框头部 */}
      <ChatHeader/>
      
      {/* 聊天消息 */}
      <div className="flex-1 overflow-auto p-4 space-y-4">
          {messages.map((message)=> (
             <div
              key={message._id}
              // 消息的发送者id和当前用户id一致,则显示在右侧,否则显示在左侧
              className={`chat ${message.senderId===authUser._id ? 'chat-end' : 'chat-start'}`}
              ref={lastMessageRef}
             >
              <div className="chat-image avatar">
                 <div className="size-10 rounded-full border">
                    <img
                      src={message.senderId === authUser._id ? authUser.profilePic || 'http://via.placeholder.com/150' : selectedUser.profilePic}
                      alt=""
                    />
                 </div>
              </div>

              <div className="chat-header mb-1">
                 <time className="text-xs opacity-50 ml-1">{formatMessageTime(message.createdAt)}</time>
              </div>
              {/* flex-col 图片和文字上下排列 */}
              <div className="chat-bubble flex flex-col"> 
                 {message.image && (
                   <img 
                    src={message.image}
                    alt=""
                    className="sm:max-w-[200px] rounded-md mb-2"
                   />
                 )}
                 {message.text && <p>{message.text}</p>}
              </div>
             </div>
          ))}

      </div>

      {/* 消息输入 */}
      <MessageInput/>
    </div>
  )
}

export default ChatBox

2、Sidebar组件

展示在线人员

javascript 复制代码
import { useEffect,useState} from "react"
import { useChatStore } from "../store/useChatStore"
import { useAuthStore } from "../store/useAuthStore"
import {User} from "lucide-react"

const Sidebar = () => {
    const {getUsers,users,selectedUser, setSelectedUser,isUsersLoading} = useChatStore()
    const {onlineUsers} = useAuthStore()
    const [showOnlyOnlineUsers, setShowOnlyOnlineUsers] = useState(false)
    // const onlineUsers = [];

    // 过滤在线用户
    const filterOnlineUsers = showOnlyOnlineUsers ? users.filter((user) => onlineUsers.includes(user._id)) :users
    useEffect(() => {
        getUsers()
    },[getUsers])

    if(isUsersLoading) return <div>Loading...</div>
  return (
    <aside className="h-full w-20 lg:w-72 border-r border-base-300 flex flex-col transition-all duration-200">
       <div className="border-b border-base-300 w-full p-5">
            <div className="flex items-center gap-2">
                <User  className="size-6" />
                <span className="font-medium hidden lg:block">联系人</span>
            </div>
            {/* 在线人员过滤 */}

            <div className="mt-3 hidden lg:flex items-center gap-2">
                <label className="cursor-pointer flex items-center gap-2">
                    <input 
                        type="checkbox"
                        checked={showOnlyOnlineUsers}
                        onChange={(e) => setShowOnlyOnlineUsers(e.target.checked)}
                        className="checkbox checkbox-sm"
                    />
                    <span className="text-sm">只显示在线</span>
                </label>
                <span className="text-xs text-zinc-500">({onlineUsers.length -1}在线)</span>
            </div>
       </div>

       <div className="overflow-y-auto w-full py-3">

         {filterOnlineUsers.map((user) =>(
            <button
                key={user._id}
                onClick={() => setSelectedUser(user)}
                className={`w-full p-3 flex items-center gap-3
                    hover:bg-base-300 transition-colors
                    ${selectedUser?._id===user._id}?"bg-base-300 ring-l ring-base-300":""
                `}
            >
                <div className="relative mx-auto lg:mx-0">
                    <img 
                        src={user.profilePic || "https://picsum.photos/200" }
                        alt={user.userName}
                        className="size-12 object-cover rounded-full"
                    
                    />
                    {onlineUsers.includes(user._id) &&(
                        <span className="absolute bottom-0 right-0 bg-green-500 size-4 rounded-full ring-2 ring-zinc-900">

                        </span>
                    )}
                </div>

                {/* 用户信息 只在大屏显示 */}
                <div className="hidden lg:block text-left min-w-0">
                    <div className="font-medium truncate">{user.userName}</div>
                    <div className="text-sm text-zinc-400">
                        {onlineUsers.includes(user._id) ? "在线" : "离线"}
                    </div>
                </div>
            </button>
         ))}

         {filterOnlineUsers.length === 0 && (
            <div className="text-center text-zinc-500 py-4">无在线用户</div>
         )}
       </div>
    </aside>
  )
}

export default Sidebar

3、ChatHeader组件

javascript 复制代码
import { useChatStore } from "../store/useChatStore";
import { useAuthStore } from "../store/useAuthStore";
import { X } from "lucide-react";

const ChatHeader = () => {
    const {selectedUser,setSelectedUser} = useChatStore();
    const {onlineUsers} = useAuthStore()
  return (
    <div className="p-2.5 border-b border-base-300">
       <div className="flex items-center justify-between">
          <div className="flex items-center">
            {/* 头像 */}
            <div className="avatar">
                <div className="size-10 rounded-full relative">
                    <img src={selectedUser.profilePic || "https://picsum.photos/200"} alt={selectedUser.userName} />
                </div>
            </div>

            {/* 用户信息 */}
            <div>
                <h3 className="font-medium">{selectedUser.userName}</h3>
                <p className="text-sm text-base-content/70">
                    {onlineUsers.includes(selectedUser._id) ? "在线" : "离线"}
                </p>
            </div>
          </div>

          {/* 关闭按钮 */}
          <button onClick={()=>setSelectedUser(null)}>
            <X/>
          </button>
       </div>
    </div>
  )
}

export default ChatHeader

这下页面体验就更好了!

相关推荐
英俊潇洒美少年1 小时前
React 最核心 3 大底层原理:Fiber + Diff + 事件系统
前端·react.js·前端框架
我命由我123451 小时前
React Router 6 - 概述、基础路由、重定向、NavLink、路由表
前端·javascript·react.js·前端框架·ecmascript·html5·js
LJianK11 小时前
java封装
java·前端·数据库
yaaakaaang1 小时前
(四)前端,如此简单!---Promise
前端·javascript
aini_lovee2 小时前
C# 实现邮件发送源码(支持附件)
开发语言·javascript·c#
GISer_Jing2 小时前
ReAct规划原理实战指南
前端·react.js·ai·aigc
SoaringHeart2 小时前
Flutter组件封装:翻转组件 NFlipCard
前端·flutter
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB查询(4)
数据库·学习·mongodb
LlNingyu2 小时前
文艺复兴,什么是XSS,常见形式(一)
前端·安全·web安全·xss
英俊潇洒美少年2 小时前
js 进程与线程的讲解
javascript