FChat开发记录 - 消息已读管理

思路

记录用户查看朋友消息的最后时间,朋友可以根据这个时间来与自己发送的消息的发送时间作比较,据此来判断消息是否已被用户查看

模型

用户的已读回执需要持久化存储,故创建了新的Mongoose模型

Read:用户id,朋友id,最后查看时间

ReadService:根据两个id来查询最后查看时间;根据两个id与当前时间新建或更新记录

场景

用户登录

socket开始监听 receive-msg 事件

receive-msg 事件:触发后更新与对应好友之间的消息列表;如果当前正打开与该好友的聊天界面,则socket向服务端发送 save-read 事件

save-read 事件:服务端事件。触发后更新传入的已读回执,如果目标用户在线,向其客户端发送 read 事件

read 事件:触发后更新本地保存的朋友已读用户的最新已读时间

用户打开与好友的聊天界面

发送请求获取该好友已读当前用户的最新已读时间(注意请求参数中,userId是当前好友的id,而friendId是当前用户的id),根据这个时间来判断消息是否已读

socket开始监听 read 事件

socket向服务端发送 save-read 事件,更新当前用户已读该好友的已读时间

用户向好友发送消息

socket向服务端发送 chat 事件

chat 事件:服务端事件。触发后对传入消息进行保存。如果目标用户在线,向其客户端发送 receive-msg 事件

解释

打开与好友的聊天界面就去获取好友已读我的时间,依此来判断我发送的消息是否已读;同时去更新数据库中我已读好友的最新时间,因为我打开界面查看了最新的消息;此时好友若在线并且打开了与我的聊天界面,那么就通过socket来通知他更新我已读他的时间;若好友不在线或未打开界面也无妨,他再次上线并打开与我的聊天界面时,会去数据库获取最新的已读时间。

发送新的消息时,如果好友在线并打开了与我的聊天界面,那么会通知我的客户端更新好友的最后已读时间,因为他在线接收了我发送的消息。

代码

服务端socket

// socket服务端

/**
 * 处理socket连接
 * @param {*} server
 */
const socketIo = require('socket.io')
const messageService = require('../services/MessageService')
const userService = require('../services/UserService')
const readService = require('../services/ReadService')

// 创建一个Map来存储每个用户的socket实例
const clientSockets = new Map()

module.exports = (server) => {
  // 连接socket
  const io = socketIo(server, { cors: { origin: '*' } })
  // 监听连接
  io.on('connection', (socket) => {
    // 存储当前用户id
    let uid = ''
    console.log('用户连接...')
    // 监听用户连接
    socket.on('login', async (userId) => {
      uid = userId
      // 更改数据库中你的状态
      userService.updateStatus(userId, 'online')
      // 告诉在线的好友你上线了
      const ids = await userService.getFriendIds(userId)
      ids.forEach((id) => {
        if (clientSockets.has(id.toString())) {
          clientSockets.get(id.toString()).emit(userId, 'online')
        }
      })
      // 保存当前用户的socket实例
      clientSockets.set(userId, socket)
      console.log('当前用户:', clientSockets.size)
    })

    // 接收到当前用户客户端发送的消息,将消息保存至数据库,然后发送给目标用户和当前用户的客户端
    socket.on('chat', async (msg) => {
      // 保存消息至数据库
      const savedMsg = await messageService.createMessage(msg.content, msg.senderId, msg.receiverId)
      // 发送给当前用户
      socket.emit('callback-msg', savedMsg)
      // 发送给目标用户
      if (clientSockets.has(msg.receiverId)) {
        clientSockets.get(msg.receiverId).emit('receive-msg', savedMsg)
      }
    })

    // 更新已读回执
    socket.on('save-read', async ([userId, friendId]) => {
      // 确定已读时间
      const time = new Date()
      // 更新回执
      await readService.updateRead(userId, friendId, time)
      // 如果朋友在线,通知其我已读
      if (clientSockets.has(friendId)) {
        clientSockets.get(friendId).emit('read', time)
      }
    })

    // 断开连接
    socket.on('disconnect', async () => {
      console.log('用户断开连接...')
      // 更改数据库中你的状态   
      userService.updateStatus(uid, 'offline')
      // 告诉在线的好友你下线了
      const ids = await userService.getFriendIds(uid)
      ids.forEach((id) => {
        if (clientSockets.has(id.toString())) {
          clientSockets.get(id.toString()).emit(uid, 'offline')
        }
      })
      // 清理存储的用户socket实例
      clientSockets.delete(uid)
      console.log('当前用户:', clientSockets.size)
    })
  })
}


客户端Chat组件(聊天界面)

/**
 * 已读未读
 */

// 初始化时更新已读回执
function updateRead() {
  socket.emit('save-read', [user.value._id, friend.value._id])

  // 并绑定接收好友更新已读回执的通知
  socket.on('read', (time) => {
    lastReadAt.value = time
  })
}

// 好友最后已读时间
const lastReadAt = ref()

// 获取最后已读时间
async function getLastRead() {
  const res = await request.get('/read', {
    params: { friendId: friend.value._id }
  })
  if (res && res.code === 200) {
    lastReadAt.value = res.data
  }
}

// 从缓存队列中恢复时/载入时
onActivated(async () => {
  await getFriend()
  load()
  getLastRead()
  receiveMsg()
  updateRead()
})
onDeactivated(() => {
  // 解绑事件
  socket.off('callback-msg')
  socket.off(friend.value._id)
  socket.off('read')
})

客户端ChatList组件(对话窗口列表)

/**
 * socket
 */
const socket = inject('socket')
// 接收到非当前好友的新消息,标记未读,更新最后消息
socket.on('receive-msg', (msg) => {
  updateAside([msg.content, msg.senderId, msg.createdTime])
  // 更新对应的消息列表
  if (msgMap.value[msg.senderId]) {
    msgMap.value[msg.senderId].messages.push(msg)
  } else {
    msgMap.value[msg.senderId] = {
      messages: [msg],
      isLastPage: true,
      nextId: msg._id
    }
  }
  // 如果是正在聊天的,滚动到底部,同时更新已读时间
  if (msg.senderId === activeItem.value) {
    bus.emit('bottom')
    socket.emit('save-read', [msg.receiverId, msg.senderId])
  }
})

参考

Web聊天室消息[已读未读]的实现 - xiepl1997 - 博客园 (cnblogs.com)

效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值