已读回执
功能描述
已读回执(Read Receipt)用于通知发送人“接收人已经阅读了发送的消息”。当接收人阅读消息后,上报消息已读,后台系统会生成一条通知,并将其发送给发送人,以告知消息已被查看。
在即时通讯工具(例如:WhatsApp、微信等)中,当接收人查看消息时,发送人会看到消息旁边的已读标记,例如蓝色的对勾或“已读”字样。
说明:
“回执”的含义是“回复的收据”,它代表了一种确认接收的凭证。当你发送一条消息,并请求一个回执,你实际上是在请求对方“我想确认你们是否接收并阅读了我的消息”。这个确认就像是一张“收据”,证明你的消息已经被接收。
已读回执有助于确保重要信息已被查看,但也可能引发心理压力和隐私问题,因此我们支持用户关闭已读回执功能。
效果展示
您可以使用本功能,实现如下图所示的消息未读、已读效果:
接口说明
设置支持已读回执的群类型
如果要支持群聊消息已读回执,需要先在 控制台 中设置支持已读回执的群类型,选中的群类型才能使用已读回执功能。配置路径:Applications > Your App > Chat > Configuration > Group Configuration > Group feature configuration > Read receipts for group messages。
单聊消息已读回执不需要在控制台做任何设置,默认支持已读回执。
发送端设置消息需要已读回执
发送端创建消息后,先通过消息对象
V2TIMMessage
的 needReadReceipt
(Android / iOS & Mac / Windows) 字段设置这条消息需要已读回执,再发送消息到会话中。示例代码如下:
V2TIMMessage message = V2TIMManager.getMessageManager().createTextMessage("群已读回执消息");// 设置消息需要已读回执message.setNeedReadReceipt(true);// 发送消息V2TIMManager.getMessageManager().sendMessage(message, null, "groupA", V2TIMMessage.V2TIM_PRIORITY_NORMAL, false, null, new V2TIMSendCallback<V2TIMMessage>() {@Overridepublic void onProgress(int progress) {}@Overridepublic void onSuccess(V2TIMMessage message) {}@Overridepublic void onError(int code, String desc) {}});
/// 接口调用示例V2TIMMessage *message = [[V2TIMManager sharedInstance] createTextMessage:@"群已读回执消息"];// 设置消息需要已读回执message.needReadReceipt = YES;// 发送消息[[V2TIMManager sharedInstance] sendMessage:message receiver:nil groupID:@"groupA" priority:V2TIM_PRIORITY_NORMAL onlineUserOnly:NO offlinePushInfo:nil progress:nil succ:nil fail:nil];
class SendCallback final : public V2TIMSendCallback {public:using SuccessCallback = std::function<void(const V2TIMMessage&)>;using ErrorCallback = std::function<void(int, const V2TIMString&)>;using ProgressCallback = std::function<void(uint32_t)>;SendCallback() = default;~SendCallback() override = default;void SetCallback(SuccessCallback success_callback, ErrorCallback error_callback,ProgressCallback progress_callback) {success_callback_ = std::move(success_callback);error_callback_ = std::move(error_callback);progress_callback_ = std::move(progress_callback);}void OnSuccess(const V2TIMMessage& message) override {if (success_callback_) {success_callback_(message);}}void OnError(int error_code, const V2TIMString& error_message) override {if (error_callback_) {error_callback_(error_code, error_message);}}void OnProgress(uint32_t progress) override {if (progress_callback_) {progress_callback_(progress);}}private:SuccessCallback success_callback_;ErrorCallback error_callback_;ProgressCallback progress_callback_;};V2TIMMessage message = V2TIMManager::GetInstance()->GetMessageManager()->CreateTextMessage(u8"群已读回执消息");// 设置消息需要已读回执message.setNeedReadReceipt(true);auto callback = new SendCallback{};callback->SetCallback([=](const V2TIMMessage& message) { delete callback; },[=](int error_code, const V2TIMString& error_message) { delete callback; },[=](uint32_t progress) {});V2TIMManager::GetInstance()->GetMessageManager()->SendMessage(message, "groupA", {}, V2TIMMessagePriority::V2TIM_PRIORITY_DEFAULT, false, {}, callback);
接收端发送消息已读回执
接收端收到消息后,根据消息对象
V2TIMMessage
的 needReadReceipt
字段判断消息是否需要已读回执。如果需要,当用户查看消息后,调用 sendMessageReadReceipts
(Android / iOS & Mac / Windows) 接口发送消息已读回执。示例代码如下:
// 假设 message 消息用户已经查看if (!message.isSelf() && message.isNeedReadReceipt()) {List<V2TIMMessage> messageList = new ArrayList<>();messageList.add(message);V2TIMManager.getMessageManager().sendMessageReadReceipts(messageList, new V2TIMCallback() {@Overridepublic void onSuccess() {// 发送消息已读回执成功}@Overridepublic void onError(int code, String desc) {// 发送消息已读回执失败}});}
/// 接口调用示例/// 假设 msg 消息用户已经查看if (!msg.isSelf && msg.needReadReceipt) {[[V2TIMManager sharedInstance] sendMessageReadReceipts:@[msg] succ:^{// 发送消息已读回执成功} fail:^(int code, NSString *desc) {// 发送消息已读回执失败}];}
class Callback final : public V2TIMCallback {public:using SuccessCallback = std::function<void()>;using ErrorCallback = std::function<void(int, const V2TIMString&)>;Callback() = default;~Callback() override = default;void SetCallback(SuccessCallback success_callback, ErrorCallback error_callback) {success_callback_ = std::move(success_callback);error_callback_ = std::move(error_callback);}void OnSuccess() override {if (success_callback_) {success_callback_();}}void OnError(int error_code, const V2TIMString& error_message) override {if (error_callback_) {error_callback_(error_code, error_message);}}private:SuccessCallback success_callback_;ErrorCallback error_callback_;};auto callback = new Callback;callback->SetCallback([=]() {// 发送消息已读回执成功delete callback;},[=](int error_code, const V2TIMString& error_message) {// 发送消息已读回执失败delete callback;});// 假设 message 消息用户已经查看if (!message.isSelf && message.needReadReceipt) {V2TIMMessageVector messageList;messageList.PushBack(message);V2TIMManager::GetInstance()->GetMessageManager()->SendMessageReadReceipts(messageList, callback);}
发送端监听消息已读回执通知
接收端发送消息已读回执后,发送端可以在
V2TIMAdvancedMsgListener
的 onRecvMessageReadReceipts
(Android / iOS & Mac / Windows) 中收到已读回执通知,在通知中更新 UI,例如更新某条消息为 “2 人已读”。示例代码如下:
V2TIMAdvancedMsgListener advancedMsgListener = new V2TIMAdvancedMsgListener() {@Overridepublic void onRecvMessageReadReceipts(List<V2TIMMessageReceipt> receiptList) {for (V2TIMMessageReceipt receipt : receiptList) {// 已读回执消息 IDString msgID = receipt.getMsgID();// C2C 消息对方 IDString userID = receipt.getUserID();// C2C 消息对方已读状态boolean isPeerRead = receipt.isPeerRead();// C2C 消息对方已读时间long timestamp = receipt.getTimestamp();// 群消息最新已读数long readCount = receipt.getReadCount();// 群消息最新未读数long unreadCount = receipt.getUnreadCount();}}};V2TIMManager.getMessageManager().addAdvancedMsgListener(advancedMsgListener);
/// 接口调用示例[[V2TIMManager sharedInstance] addAdvancedMsgListener:self];- (void)onRecvMessageReadReceipts:(NSArray<V2TIMMessageReceipt *> *)receiptList {for(V2TIMMessageReceipt *receipt in receiptList) {// 已读回执消息 IDNSString *msgID = receipt.msgID;// C2C 消息对方 IDNSString * userID = receipt.userID;// C2C 消息对方已读状态BOOL isPeerRead = receipt.isPeerRead;// C2C 消息对方已读时间time_t timestamp = receipt.timestamp;// 群组 IDNSString * groupID = receipt.groupID;// 群消息最新已读数uint64_t readCount = receipt.readCount;// 群消息最新未读数uint64_t unreadCount = receipt.unreadCount;}}
class AdvancedMsgListener final : public V2TIMAdvancedMsgListener {public:/*** 消息已读回执通知** @param receiptList 已读回执列表*/void OnRecvMessageReadReceipts(const V2TIMMessageReceiptVector& receiptList) override {for (size_t i = 0; i < receiptList.Size(); ++i) {const V2TIMMessageReceipt& receipt = receiptList[i];// 已读回执消息 IDV2TIMString msgID = receipt.msgID;// C2C 消息对方 IDV2TIMString userID = receipt.userID;// C2C 消息对方已读状态bool isPeerRead = receipt.isPeerRead;// C2C 消息对方已读时间int64_t timestamp = receipt.timestamp;// 群组 IDV2TIMString groupID = receipt.groupID;// 群消息最新已读数int32_t readCount = receipt.readCount;// 群消息最新未读数int32_t unreadCount = receipt.unreadCount;}}// 其他成员 ...};// 添加高级消息的事件监听器,注意在移除监听器之前需要保持 advancedMsgListener 的生命期,以免接收不到事件回调AdvancedMsgListener advancedMsgListener;V2TIMManager::GetInstance()->GetMessageManager()->AddAdvancedMsgListener(&advancedMsgListener);
发送端主动拉取消息已读回执信息
其中已读回执信息
V2TIMMessageReceipt
字段含义如下:属性 | 含义 | 说明 |
msgID | 消息 ID | 消息唯一 ID。 |
userID | 对端用户 ID | 如果是单聊,该字段表示对端用户 ID。 |
isPeerRead | 消息对端用户是否已读 | 如果是单聊,该字段表示消息对端用户是否已读。 |
timestamp | 对端用户已读时间 | 如果 msgID 为空,该字段表示对端用户标记会话已读的时间。 如果 msgID 不为空,该字段表示对端用户发送消息已读回执的时间(8.1 及以上版本支持)。 |
groupID | 群组 ID | 如果是群聊,该字段为群组 ID。 |
readCount | 群消息已读人数 | 如果是群聊,该字段为群消息已读人数。 |
unreadCount | 群消息未读人数 | 如果是群聊,该字段为群消息未读人数。 |
示例代码如下:
/// 接口调用示例(以 Group 消息为例)V2TIMManager.getMessageManager().getGroupHistoryMessageList("groupA", 20, null, new V2TIMValueCallback<List<V2TIMMessage>>() {@Overridepublic void onSuccess(final List<V2TIMMessage> v2TIMMessages) {List<V2TIMMessage> receiptMsgs = new ArrayList<>();// 自己发送的消息 && 需要已读回执,需要拉取消息的已读回执信息for (V2TIMMessage msg : v2TIMMessages) {if (msg.isSelf() && msg.isNeedReadReceipt()) {receiptMsgs.add(msg);}}V2TIMManager.getMessageManager().getMessageReadReceipts(receiptMsgs, new V2TIMValueCallback<List<V2TIMMessageReceipt>>() {@Overridepublic void onSuccess(List<V2TIMMessageReceipt> v2TIMMessageReceipts) {Map<String, V2TIMMessageReceipt> messageReceiptMap = new HashMap<>();for (V2TIMMessageReceipt receipt : v2TIMMessageReceipts) {messageReceiptMap.put(receipt.getMsgID(), receipt);}for (V2TIMMessage msg : v2TIMMessages) {V2TIMMessageReceipt receipt = messageReceiptMap.get(msg.getMsgID());if (receipt != null) {// C2C 消息对方 IDString userID = receipt.getUserID();// C2C 消息对方已读状态boolean isPeerRead = receipt.isPeerRead();// C2C 消息对方已读时间long timestamp = receipt.getTimestamp();// 群组 IDString groupID = receipt.getGroupID();// 消息已读数,readCount 为 0,表示消息无人已读long readCount = receipt.getReadCount();// 消息未读数,unreadCount 为 0,表示消息全部已读long unreadCount = receipt.getUnreadCount();}}}@Overridepublic void onError(int code, String desc) {// 拉取消息已读状态失败}});}@Overridepublic void onError(int code, String desc) {// 拉取消息失败}});
/// 接口调用示例(以 Group 消息为例)[[V2TIMManager sharedInstance] getGroupHistoryMessageList:@"groupA" count:20 lastMsg:nil succ:^(NSArray<V2TIMMessage *> *msgs) {NSMutableArray *receiptMsgs = [NSMutableArray array];// 自己发送的消息 && 需要已读回执,需要拉取消息的已读回执信息for (V2TIMMessage *msg in msgs) {if (msg.isSelf && msg.needReadReceipt) {[receiptMsgs addObject:msg];}}[[V2TIMManager sharedInstance] getMessageReadReceipts:receiptMsgs succ:^(NSArray<V2TIMMessageReceipt *> *receiptList) {NSMutableDictionary *param = [NSMutableDictionary dictionary];for (V2TIMMessageReceipt *receipt in receiptList) {[param setObject:receipt forKey:receipt.msgID];}for (V2TIMMessage *msg in msgs) {V2TIMMessageReceipt *receipt = param[msg.msgID];// C2C 消息对方 IDNSString * userID = receipt.userID;// C2C 消息对方已读状态BOOL isPeerRead = receipt.isPeerRead;// C2C 消息对方已读时间time_t timestamp = receipt.timestamp;// 群组 IDNSString * groupID = receipt.groupID;// 消息已读数,readCount 为 0,表示消息无人已读uint64_t readCount = receipt.readCount;// 消息未读数,unreadCount 为 0,表示消息全部已读uint64_t unreadCount = receipt.unreadCount;}} fail:^(int code, NSString *desc) {// 拉取消息已读状态失败}];} fail:^(int code, NSString *desc) {// 拉取消息失败}];
template <class T>class ValueCallback final : public V2TIMValueCallback<T> {public:using SuccessCallback = std::function<void(const T&)>;using ErrorCallback = std::function<void(int, const V2TIMString&)>;ValueCallback() = default;~ValueCallback() override = default;void SetCallback(SuccessCallback success_callback, ErrorCallback error_callback) {success_callback_ = std::move(success_callback);error_callback_ = std::move(error_callback);}void OnSuccess(const T& value) override {if (success_callback_) {success_callback_(value);}}void OnError(int error_code, const V2TIMString& error_message) override {if (error_callback_) {error_callback_(error_code, error_message);}}private:SuccessCallback success_callback_;ErrorCallback error_callback_;};// 接口调用示例(以 Group 消息为例)V2TIMMessageListGetOption option;option.getType = V2TIMMessageGetType::V2TIM_GET_CLOUD_OLDER_MSG; // 拉取云端的更老的消息option.count = 20; // 每页 20 条option.groupID = "groupA"; // 拉取群聊消息auto callback = new ValueCallback<V2TIMMessageVector>{};callback->SetCallback([=](const V2TIMMessageVector& messageList) {V2TIMMessageVector receiptMsgs;for (size_t i = 0; i < messageList.Size(); ++i) {const V2TIMMessage& message = messageList[0];// 自己发送的消息 && 需要已读回执,需要拉取消息的已读回执信息if (message.isSelf && message.needReadReceipt) {receiptMsgs.PushBack(message);}}auto receipt_callback = new ValueCallback<V2TIMMessageReceiptVector>{};receipt_callback->SetCallback([=](const V2TIMMessageReceiptVector& receiptList) {std::unordered_map<V2TIMString, V2TIMMessageReceipt> map;for (size_t i = 0; i < receiptList.Size(); ++i) {const V2TIMMessageReceipt& receipt = receiptList[i];map[receipt.msgID] = receipt;}for (size_t i = 0; i < messageList.Size(); ++i) {const V2TIMMessage& message = messageList[0];if (map.count(message.msgID)) {const V2TIMMessageReceipt& receipt = map[message.msgID];// C2C 消息对方 IDV2TIMString userID = receipt.userID;// C2C 消息对方已读状态bool isPeerRead = receipt.isPeerRead;// C2C 消息对方已读时间int64_t timestamp = receipt.timestamp;// 群组 IDV2TIMString groupID = receipt.groupID;// 消息已读数,readCount 为 0,表示消息无人已读int32_t readCount = receipt.readCount;// 消息未读数,unreadCount 为 0,表示消息全部已读int32_t unreadCount = receipt.unreadCount;}const V2TIMMessageReceipt& receipt = receiptList[i];}delete receipt_callback;},[=](int error_code, const V2TIMString& error_message) {// 拉取消息已读状态失败delete receipt_callback;});V2TIMManager::GetInstance()->GetMessageManager()->GetMessageReadReceipts(messageList, receipt_callback);delete callback;},[=](int error_code, const V2TIMString& error_message) {// 拉取消息失败delete callback;});V2TIMManager::GetInstance()->GetMessageManager()->GetHistoryMessageList(option, callback);
发送端主动拉取群消息已读或未读成员列表
发送端在需要查看群消息已读或未读成员列表时,可以调用
getGroupMessageReadMemberList
(Android / iOS & Mac / Windows) 接口分页拉取消息已读或未读群成员列表。/// 接口调用示例V2TIMManager.getMessageManager().getGroupMessageReadMemberList(message, V2TIMMessage.V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ, 0, 100, new V2TIMValueCallback<V2TIMGroupMessageReadMemberList>() {@Overridepublic void onSuccess(V2TIMGroupMessageReadMemberList v2TIMGroupMessageReadMemberList) {// members 当前分页拉取的已读成员列表List<V2TIMGroupMemberInfo> members = v2TIMGroupMessageReadMemberList.getMemberInfoList();// nextSeq 下次分页拉取的游标位置long nextSeq = v2TIMGroupMessageReadMemberList.getNextSeq();// isFinished 已读列表是否已经全部拉取完毕boolean isFinished = v2TIMGroupMessageReadMemberList.isFinished();// 如果已读列表没有全部拉取完毕,继续下一页拉取(这里只是示例代码,分页拉取建议由用户点击 UI 触发)if (!isFinished) {V2TIMManager.getMessageManager().getGroupMessageReadMemberList(message, V2TIMMessage.V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ, nextSeq, 100, new V2TIMValueCallback<V2TIMGroupMessageReadMemberList>() {@Overridepublic void onSuccess(V2TIMGroupMessageReadMemberList v2TIMGroupMessageReadMemberList) {// 拉群已读成员列表成功}@Overridepublic void onError(int code, String desc) {// 拉群已读成员列表失败}});}}@Overridepublic void onError(int code, String desc) {// 拉群已读成员列表失败}});
/// 接口调用示例[[V2TIMManager sharedInstance] getGroupMessageReadMemberList:message filter:V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ nextSeq:0 count:100 succ:^(NSMutableArray<V2TIMGroupMemberInfo *> *members, uint64_t nextSeq, BOOL isFinished) {// members 当前分页拉取的已读成员列表// nextSeq 下次分页拉取的游标位置// isFinished 已读列表是否已经全部拉取完毕// 如果已读列表没有全部拉取完毕,继续下一页拉取(这里只是示例代码,分页拉取建议由用户点击 UI 触发)if (!isFinished) {[[V2TIMManager sharedInstance] getGroupMessageReadMemberList:message filter:V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ nextSeq:nextSeq count:100 succ:^(NSMutableArray<V2TIMGroupMemberInfo *> *members, uint64_t nextSeq, BOOL isFinished) {// 拉群已读成员列表成功} fail:^(int code, NSString *desc) {// 拉群已读成员列表失败}];}} fail:^(int code, NSString *desc) {// 拉群已读成员列表失败}];
template <class T>class ValueCallback final : public V2TIMValueCallback<T> {public:using SuccessCallback = std::function<void(const T&)>;using ErrorCallback = std::function<void(int, const V2TIMString&)>;ValueCallback() = default;~ValueCallback() override = default;void SetCallback(SuccessCallback success_callback, ErrorCallback error_callback) {success_callback_ = std::move(success_callback);error_callback_ = std::move(error_callback);}void OnSuccess(const T& value) override {if (success_callback_) {success_callback_(value);}}void OnError(int error_code, const V2TIMString& error_message) override {if (error_callback_) {error_callback_(error_code, error_message);}}private:SuccessCallback success_callback_;ErrorCallback error_callback_;};auto callback = new ValueCallback<V2TIMGroupMessageReadMemberList>{};callback->SetCallback([=](const V2TIMGroupMessageReadMemberList& groupMessageReadMemberList) {// members 当前分页拉取的已读成员列表const V2TIMGroupMemberInfoVector& members = groupMessageReadMemberList.members;// nextSeq 下次分页拉取的游标位置uint64_t nextSeq = groupMessageReadMemberList.nextSeq;// isFinished 已读列表是否已经全部拉取完毕bool isFinished = groupMessageReadMemberList.isFinished;// 如果已读列表没有全部拉取完毕,继续下一页拉取(这里只是示例代码,分页拉取建议由用户点击 UI 触发)if (!isFinished) {auto callback = new ValueCallback<V2TIMGroupMessageReadMemberList>{};callback->SetCallback([=](const V2TIMGroupMessageReadMemberList& groupMessageReadMemberList) {// 拉群已读成员列表成功delete callback;},[=](int error_code, const V2TIMString& error_message) {// 拉群已读成员列表失败delete callback;});V2TIMManager::GetInstance()->GetMessageManager()->GetGroupMessageReadMemberList(message, V2TIMGroupMessageReadMembersFilter::V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ,nextSeq, 100, callback);}delete callback;},[=](int error_code, const V2TIMString& error_message) {// 拉群已读成员列表失败delete callback;});V2TIMManager::GetInstance()->GetMessageManager()->GetGroupMessageReadMemberList(message, V2TIMGroupMessageReadMembersFilter::V2TIM_GROUP_MESSAGE_READ_MEMBERS_FILTER_READ, 0, 100,callback);