You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于PubNub的聊天应用实时未读消息计数功能实现求助

问题:实现实时未读消息计数与UI展示

我正在用PubNub SDK开发聊天应用,技术栈如下:

  • 服务端:Express.js + Prisma ORM + MySQL
  • 客户端:React.js

应用流程:

  1. 用户登录后,根据用户ID获取会话列表;
  2. 用户点击会话时,根据会话ID调用API获取消息,并通过PubNub订阅对应频道、注册事件监听器,实现实时消息收发。

当前需求:

  • 用户A给用户B发消息时,若用户B未订阅该会话频道,消息标记为未读;
  • 用户B订阅该频道时,自动将该会话的所有未读消息标记为已读;
  • 实现实时未读消息计数并在UI展示。

数据库模型

model User {
  id            String         @id @default(uuid())
  email         String         @unique
  name          String?
  password      String
  createdAt     DateTime       @default(now())
  conversations Conversation[]
  messages      Message[]
  participants  Participant[]
}

model Conversation {
  id               String        @id @default(uuid())
  conversationName String?
  type             String        @default("1vs1")
  creatorUserId    String
  creatorUser      User          @relation(fields: [creatorUserId], references: [id], onDelete: Cascade)
  timestamp        DateTime      @default(now())
  messages         Message[]
  participants     Participant[]
}

model Message {
  id             String        @id @default(uuid())
  conversationId String
  conversation   Conversation  @relation(fields: [conversationId], references: [id], onDelete: Cascade)
  senderUserId   String
  senderUser     User          @relation(fields: [senderUserId], references: [id], onDelete: Cascade)
  content        String?
  messageType    String
  fileUrl        String?
  emojiCode      String?
  timestamp      DateTime      @default(now())
  isRead         Boolean       @default(false)
}

model Participant {
  id                String       @id @default(uuid())
  conversationId    String
  conversation      Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
  userId            String
  user              User         @relation(fields: [userId], references: [id], onDelete: Cascade)
}

解决方案

一、服务端修改

1. 新增会话列表的未读消息计数逻辑

修改获取用户会话列表的API,计算每个会话中当前用户的未读消息数:

// conversationAPI.js
async function getConversationsForUser(userId) {
  const conversations = await prisma.conversation.findMany({
    where: {
      participants: { some: { userId } }
    },
    include: {
      participants: { include: { user: true } },
      messages: { take: 1, orderBy: { timestamp: 'desc' } }
    },
    orderBy: { timestamp: 'desc' }
  });

  return Promise.all(conversations.map(async (conversation) => {
    // 统计当前用户的未读消息数(排除自己发送的)
    const unreadCount = await prisma.message.count({
      where: {
        conversationId: conversation.id,
        senderUserId: { not: userId },
        isRead: false
      }
    });
    // 提取会话对方的信息(排除当前用户)
    const participantInfo = conversation.participants.find(p => p.userId !== userId)?.user;
    
    return {
      ...conversation,
      participantInfo,
      unreadMessageCount: unreadCount
    };
  }));
}

2. 新增批量标记消息为已读的API

创建接口用于用户订阅会话时,批量更新未读状态:

// messageAPI.js
async function markMessagesAsRead(conversationId, userId) {
  return await prisma.message.updateMany({
    where: {
      conversationId,
      senderUserId: { not: userId },
      isRead: false
    },
    data: { isRead: true }
  });
}

二、客户端修改

1. 订阅会话时标记消息为已读

修改MessageListComponent,在订阅频道后调用标记已读接口,并触发会话列表更新:

// MessageListComponent.js
useEffect(() => {
  const markMessagesRead = async () => {
    try {
      await messageAPIs.markMessagesAsRead(conversationId, user.id);
      // 触发自定义事件通知Chat组件更新会话列表
      window.dispatchEvent(new CustomEvent('updateConversations'));
    } catch (error) {
      console.error('标记消息已读失败:', error);
    }
  };

  addListeners(pn, {
    message: handleNewMessage,
    presence: handlePresence
  });
  subscribeToChannels(pn, [conversationId]);
  markMessagesRead(); // 订阅频道时立即标记已读

  return () => {
    pn.removeListener({ message: handleNewMessage, presence: handlePresence });
    unsubscribeAll(pn);
  };
}, [conversationId, pn]);

2. 监听会话更新事件,刷新会话列表

修改Chat组件,添加自定义事件监听,实时更新会话的未读计数:

// Chat.js
useEffect(() => {
  const updateConversations = async () => {
    try {
      const response = await conversationAPIs.getConversationsForUser(user.id);
      setConversations(response.data.conversations);
    } catch (error) {
      setError(error);
    }
  };

  // 监听自定义事件,更新会话列表
  window.addEventListener('updateConversations', updateConversations);

  // 初始加载会话
  const fetchConversations = async () => {
    try {
      const response = await conversationAPIs.getConversationsForUser(user.id);
      setConversations(response.data.conversations);
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  };
  fetchConversations();

  return () => window.removeEventListener('updateConversations', updateConversations);
}, [user.id]);

3. 实时接收消息时更新未读计数

修改MessageListComponent的消息监听逻辑,收到新消息后触发会话列表更新:

// MessageListComponent.js
const handleNewMessage = useCallback((envelope) => {
  setMessages(prevMessages => [...prevMessages, envelope.message]);
  // 如果当前用户是消息接收者,更新会话未读计数
  if (envelope.message.senderUserId !== user.id) {
    window.dispatchEvent(new CustomEvent('updateConversations'));
  }
}, [user.id]);

4. 完善未读计数的UI展示

确保Conversation组件正确渲染未读计数(示例):

// Conversation.js
function Conversation({ name, convoId, date, unread, onClick }) {
  return (
    <div onClick={onClick} className="p-3 border-b hover:bg-gray-50 cursor-pointer">
      <div className="flex justify-between items-start">
        <h3 className="font-medium">{name}</h3>
        {unread > 0 && (
          <span className="bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
            {unread}
          </span>
        )}
      </div>
      <p className="text-sm text-gray-500">{date}</p>
    </div>
  );
}

三、可选:利用PubNub Presence优化在线状态检测

通过Presence事件精准检测用户加入频道的动作,触发标记已读:

// MessageListComponent.js
const handlePresence = useCallback((e) => {
  const { action, uuid } = e;
  // 当前用户加入频道时标记消息已读
  if (action === 'join' && uuid === user.id) {
    messageAPIs.markMessagesAsRead(conversationId, user.id)
      .then(() => window.dispatchEvent(new CustomEvent('updateConversations')))
      .catch(err => console.error(err));
  }
}, [user.id, conversationId]);

内容的提问来源于stack exchange,提问作者Nguyễn Văn Tiến

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 11:32:02