The message data sources that need to be rendered on the page mainly include the following:
Historical conversation messages: When you first enter the chat page and view historical messages, you need to query historical conversation messages and render them to the chat interface.
Real-time received messages: Real-time received chat messages need to be rendered to the chat interface.
Local sent messages: Local sent chat messages (including sending, sending successfully, and sending failed statuses) need to be rendered to the chat interface.
Get Message Data and Render to the Chat Interface
As an example of a single chat conversation, when you navigate to the chat page, you need to save the corresponding single chat conversationID (the userID of the other party) and maintain a message list myMessageList for saving the current conversation.
Before obtaining message data, you also need to implement an addMessage utility method to merge message data.
public void addMessage(List<ZIMMessage> addList) {
if (addList == null || addList.isEmpty()) return;
List<ZIMMessage> mutableList = new ArrayList<>();
if (myMessageList != null) {
mutableList.addAll(myMessageList);
}
for (ZIMMessage newMsg : addList) {
boolean replaced = false;
for (int i = 0; i < mutableList.size(); i++) {
ZIMMessage oldMsg = mutableList.get(i);
if ((newMsg.getMessageID() != null && newMsg.getMessageID().equals(oldMsg.getMessageID())) ||
(newMsg.getLocalMessageID() != null && newMsg.getLocalMessageID().equals(oldMsg.getLocalMessageID()))) {
mutableList.set(i, newMsg);
replaced = true;
break;
}
}
if (!replaced) {
mutableList.add(newMsg);
}
}
mutableList.sort(Comparator.comparingLong(ZIMMessage::getOrderKey));
myMessageList = mutableList;
}
public void addMessage(List<ZIMMessage> addList) {
if (addList == null || addList.isEmpty()) return;
List<ZIMMessage> mutableList = new ArrayList<>();
if (myMessageList != null) {
mutableList.addAll(myMessageList);
}
for (ZIMMessage newMsg : addList) {
boolean replaced = false;
for (int i = 0; i < mutableList.size(); i++) {
ZIMMessage oldMsg = mutableList.get(i);
if ((newMsg.getMessageID() != null && newMsg.getMessageID().equals(oldMsg.getMessageID())) ||
(newMsg.getLocalMessageID() != null && newMsg.getLocalMessageID().equals(oldMsg.getLocalMessageID()))) {
mutableList.set(i, newMsg);
replaced = true;
break;
}
}
if (!replaced) {
mutableList.add(newMsg);
}
}
mutableList.sort(Comparator.comparingLong(ZIMMessage::getOrderKey));
myMessageList = mutableList;
}
// When there are new messages, update the messages to the message list through this method
- (void)addMessage:(NSArray<ZIMMessage *> *)addList {
if (!addList || addList.count == 0) return;
NSMutableArray<ZIMMessage *> *mutableList = [self.myMessageList mutableCopy];
if (!mutableList) {
mutableList = [NSMutableArray array];
}
for (ZIMMessage *newMsg in addList) {
BOOL replaced = NO;
for (NSInteger i = 0; i < mutableList.count; i++) {
ZIMMessage *oldMsg = mutableList[i];
if ((newMsg.messageID && oldMsg.messageID && [newMsg.messageID isEqualToString:oldMsg.messageID]) ||
(newMsg.localMessageID && oldMsg.localMessageID && [newMsg.localMessageID isEqualToString:oldMsg.localMessageID])) {
// Replace existing messages
mutableList[i] = newMsg;
replaced = YES;
break;
}
}
if (!replaced) {
[mutableList addObject:newMsg];
}
}
// Sort by orderKey from small to large
[mutableList sortUsingComparator:^NSComparisonResult(ZIMMessage *msg1, ZIMMessage *msg2) {
if (msg1.orderKey < msg2.orderKey) return NSOrderedAscending;
if (msg1.orderKey > msg2.orderKey) return NSOrderedDescending;
return NSOrderedSame;
}];
self.myMessageList = [mutableList copy];
}
// When there are new messages, update the messages to the message list through this method
- (void)addMessage:(NSArray<ZIMMessage *> *)addList {
if (!addList || addList.count == 0) return;
NSMutableArray<ZIMMessage *> *mutableList = [self.myMessageList mutableCopy];
if (!mutableList) {
mutableList = [NSMutableArray array];
}
for (ZIMMessage *newMsg in addList) {
BOOL replaced = NO;
for (NSInteger i = 0; i < mutableList.count; i++) {
ZIMMessage *oldMsg = mutableList[i];
if ((newMsg.messageID && oldMsg.messageID && [newMsg.messageID isEqualToString:oldMsg.messageID]) ||
(newMsg.localMessageID && oldMsg.localMessageID && [newMsg.localMessageID isEqualToString:oldMsg.localMessageID])) {
// Replace existing messages
mutableList[i] = newMsg;
replaced = YES;
break;
}
}
if (!replaced) {
[mutableList addObject:newMsg];
}
}
// Sort by orderKey from small to large
[mutableList sortUsingComparator:^NSComparisonResult(ZIMMessage *msg1, ZIMMessage *msg2) {
if (msg1.orderKey < msg2.orderKey) return NSOrderedAscending;
if (msg1.orderKey > msg2.orderKey) return NSOrderedDescending;
return NSOrderedSame;
}];
self.myMessageList = [mutableList copy];
}
void addMessage(const std::vector<std::shared_ptr<ZIMMessage>>& addList) {
if (addList.empty()) return;
std::vector<std::shared_ptr<ZIMMessage>> mutableList = myMessageList;
for (auto& newMsg : addList) {
bool replaced = false;
for (size_t i = 0; i < mutableList.size(); ++i) {
auto& oldMsg = mutableList[i];
if ((!newMsg->messageID.empty() && newMsg->messageID == oldMsg->messageID) ||
(!newMsg->localMessageID.empty() && newMsg->localMessageID == oldMsg->localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.push_back(newMsg);
}
}
std::sort(mutableList.begin(), mutableList.end(),
[](const std::shared_ptr<ZIMMessage>& a, const std::shared_ptr<ZIMMessage>& b) {
return a->orderKey < b->orderKey;
});
myMessageList = mutableList;
}
void addMessage(const std::vector<std::shared_ptr<ZIMMessage>>& addList) {
if (addList.empty()) return;
std::vector<std::shared_ptr<ZIMMessage>> mutableList = myMessageList;
for (auto& newMsg : addList) {
bool replaced = false;
for (size_t i = 0; i < mutableList.size(); ++i) {
auto& oldMsg = mutableList[i];
if ((!newMsg->messageID.empty() && newMsg->messageID == oldMsg->messageID) ||
(!newMsg->localMessageID.empty() && newMsg->localMessageID == oldMsg->localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.push_back(newMsg);
}
}
std::sort(mutableList.begin(), mutableList.end(),
[](const std::shared_ptr<ZIMMessage>& a, const std::shared_ptr<ZIMMessage>& b) {
return a->orderKey < b->orderKey;
});
myMessageList = mutableList;
}
function addMessage(addList: ZIMMessage[]) {
if (!addList || addList.length === 0) return;
const mutableList: ZIMMessage[] = myMessageList ? [...myMessageList] : [];
for (const newMsg of addList) {
let replaced = false;
for (let i = 0; i < mutableList.length; i++) {
const oldMsg = mutableList[i];
if ((newMsg.messageID && oldMsg.messageID && newMsg.messageID === oldMsg.messageID) ||
(newMsg.localMessageID && oldMsg.localMessageID && newMsg.localMessageID === oldMsg.localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.push(newMsg);
}
}
mutableList.sort((a, b) => a.orderKey - b.orderKey);
myMessageList = mutableList;
}
function addMessage(addList: ZIMMessage[]) {
if (!addList || addList.length === 0) return;
const mutableList: ZIMMessage[] = myMessageList ? [...myMessageList] : [];
for (const newMsg of addList) {
let replaced = false;
for (let i = 0; i < mutableList.length; i++) {
const oldMsg = mutableList[i];
if ((newMsg.messageID && oldMsg.messageID && newMsg.messageID === oldMsg.messageID) ||
(newMsg.localMessageID && oldMsg.localMessageID && newMsg.localMessageID === oldMsg.localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.push(newMsg);
}
}
mutableList.sort((a, b) => a.orderKey - b.orderKey);
myMessageList = mutableList;
}
void addMessage(List<ZIMMessage>? addList) {
if (addList == null || addList.isEmpty) return;
List<ZIMMessage> mutableList = myMessageList != null ? List.from(myMessageList!) : [];
for (var newMsg in addList) {
bool replaced = false;
for (var i = 0; i < mutableList.length; i++) {
var oldMsg = mutableList[i];
if ((newMsg.messageID != null && oldMsg.messageID != null && newMsg.messageID == oldMsg.messageID) ||
(newMsg.localMessageID != null && oldMsg.localMessageID != null && newMsg.localMessageID == oldMsg.localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.add(newMsg);
}
}
mutableList.sort((a, b) => a.orderKey.compareTo(b.orderKey));
myMessageList = mutableList;
}
void addMessage(List<ZIMMessage>? addList) {
if (addList == null || addList.isEmpty) return;
List<ZIMMessage> mutableList = myMessageList != null ? List.from(myMessageList!) : [];
for (var newMsg in addList) {
bool replaced = false;
for (var i = 0; i < mutableList.length; i++) {
var oldMsg = mutableList[i];
if ((newMsg.messageID != null && oldMsg.messageID != null && newMsg.messageID == oldMsg.messageID) ||
(newMsg.localMessageID != null && oldMsg.localMessageID != null && newMsg.localMessageID == oldMsg.localMessageID)) {
mutableList[i] = newMsg;
replaced = true;
break;
}
}
if (!replaced) {
mutableList.add(newMsg);
}
}
mutableList.sort((a, b) => a.orderKey.compareTo(b.orderKey));
myMessageList = mutableList;
}
Note
Each time you call a message-related interface (such as the interfaces shown below), you will get a message list ZIMMessage[]. The obtained message list needs to be merged to obtain a message list without duplicate messages and ordered messages for rendering on the message page. The message sorting rule is to sort by the orderkey property in ZIMMessage (the larger the orderkey, the newer the current message); the de-duplication rule is to determine whether the message object is the same message based on the localMessageID of the message, and replace the new data with the old data when it is duplicated.
Query Historical Messages and Render Them to the Chat Interface
Call the queryHistoryMessage interface to query historical messages from new to old (from the most recent message to the oldest message in chronological order) and render them to the chat interface.
ZIMMessageQueryConfig queryConfig = new ZIMMessageQueryConfig();
queryConfig.count = 20;
queryConfig.nextMessage = null; // When the first query is empty, the earliest message is passed in when more historical messages are needed
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.PEER,
queryConfig,
(conversationID, conversationType, messageList, errorInfo) -> {
addMessage(messageList);
// update UI
});
ZIMMessageQueryConfig queryConfig = new ZIMMessageQueryConfig();
queryConfig.count = 20;
queryConfig.nextMessage = null; // When the first query is empty, the earliest message is passed in when more historical messages are needed
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.PEER,
queryConfig,
(conversationID, conversationType, messageList, errorInfo) -> {
addMessage(messageList);
// update UI
});
ZIMMessageQueryConfig *queryConfig = [[ZIMMessageQueryConfig alloc] init];
queryConfig.count = 20;//
queryConfig.nextMessage = nil;//When the first query is empty, the earliest message in the message list is passed in again when more historical messages are needed
[zim queryHistoryMessageByConversationID:myConversationID
conversationType:ZIMConversationTypePeer
config:queryConfig
callback:callback:^(NSString * _Nonnull conversationID, ZIMConversationType conversationType, NSArray<ZIMMessage *> * _Nonnull messageList, ZIMError * _Nonnull errorInfo){
[self addMessage:messageList];
// update UI
}];
ZIMMessageQueryConfig *queryConfig = [[ZIMMessageQueryConfig alloc] init];
queryConfig.count = 20;//
queryConfig.nextMessage = nil;//When the first query is empty, the earliest message in the message list is passed in again when more historical messages are needed
[zim queryHistoryMessageByConversationID:myConversationID
conversationType:ZIMConversationTypePeer
config:queryConfig
callback:callback:^(NSString * _Nonnull conversationID, ZIMConversationType conversationType, NSArray<ZIMMessage *> * _Nonnull messageList, ZIMError * _Nonnull errorInfo){
[self addMessage:messageList];
// update UI
}];
ZIMMessageQueryConfig queryConfig;
queryConfig.count = 20;
queryConfig.nextMessage = nullptr; // When the first query is empty
zim->queryHistoryMessage(
myConversationID,
ZIMConversationType::Peer,
queryConfig,
[this](const std::string& conversationID, ZIMConversationType type,
const std::vector<ZIMMessage>& messageList, const ZIMError& errorInfo) {
addMessage(messageList);
refreshUI(); // Custom UI refresh function
});
ZIMMessageQueryConfig queryConfig;
queryConfig.count = 20;
queryConfig.nextMessage = nullptr; // When the first query is empty
zim->queryHistoryMessage(
myConversationID,
ZIMConversationType::Peer,
queryConfig,
[this](const std::string& conversationID, ZIMConversationType type,
const std::vector<ZIMMessage>& messageList, const ZIMError& errorInfo) {
addMessage(messageList);
refreshUI(); // Custom UI refresh function
});
const queryConfig: ZIMMessageQueryConfig = {
count: 20,
nextMessage: null // When the first query is empty
};
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.Peer,
queryConfig,
(conversationID, conversationType, messageList, errorInfo) => {
addMessage(messageList);
updateUI(); // Custom refresh message list function
}
);
const queryConfig: ZIMMessageQueryConfig = {
count: 20,
nextMessage: null // When the first query is empty
};
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.Peer,
queryConfig,
(conversationID, conversationType, messageList, errorInfo) => {
addMessage(messageList);
updateUI(); // Custom refresh message list function
}
);
final queryConfig = ZIMMessageQueryConfig();
queryConfig.count = 20;
queryConfig.nextMessage = null; // When the first query is empty
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.peer,
queryConfig,
(String conversationID, ZIMConversationType conversationType,
List<ZIMMessage> messageList, ZIMError errorInfo) {
addMessage(messageList);
// Update UI
},
);
final queryConfig = ZIMMessageQueryConfig();
queryConfig.count = 20;
queryConfig.nextMessage = null; // When the first query is empty
zim.queryHistoryMessage(
myConversationID,
ZIMConversationType.peer,
queryConfig,
(String conversationID, ZIMConversationType conversationType,
List<ZIMMessage> messageList, ZIMError errorInfo) {
addMessage(messageList);
// Update UI
},
);
Get Real-time Received Messages and Render Them to the Chat Interface
Listen to the peerMessageReceived interface to get real-time received messages and render them to the chat interface.
public void onPeerMessageReceived(ZIM zim, List<ZIMMessage> messageList,
ZIMMessageReceivedInfo info, String fromUserID) {
// Only process messages from the current conversation
if (!fromUserID.equals(myConversationID)) {
return; // Messages from non-current conversations are ignored
}
// Merge messages into message list
addMessage(messageList);
// Update UI, such as refreshing RecyclerView
runOnUiThread(() -> {
myAdapter.notifyDataSetChanged();
if (!myMessageList.isEmpty()) {
recyclerView.scrollToPosition(myMessageList.size() - 1);
}
});
}
public void onPeerMessageReceived(ZIM zim, List<ZIMMessage> messageList,
ZIMMessageReceivedInfo info, String fromUserID) {
// Only process messages from the current conversation
if (!fromUserID.equals(myConversationID)) {
return; // Messages from non-current conversations are ignored
}
// Merge messages into message list
addMessage(messageList);
// Update UI, such as refreshing RecyclerView
runOnUiThread(() -> {
myAdapter.notifyDataSetChanged();
if (!myMessageList.isEmpty()) {
recyclerView.scrollToPosition(myMessageList.size() - 1);
}
});
}
void onPeerMessageReceived(ZIM* zim, const std::vector<ZIMMessage>& messageList,
const ZIMMessageReceivedInfo& info, const std::string& fromUserID) {
// Only process messages from the current conversation
if (fromUserID != myConversationID) {
return; // Messages from non-current conversations are ignored
}
// Merge messages into message list
addMessage(messageList);
// Update UI
// Assume there is a refresh function refreshUI()
refreshUI();
}
void onPeerMessageReceived(ZIM* zim, const std::vector<ZIMMessage>& messageList,
const ZIMMessageReceivedInfo& info, const std::string& fromUserID) {
// Only process messages from the current conversation
if (fromUserID != myConversationID) {
return; // Messages from non-current conversations are ignored
}
// Merge messages into message list
addMessage(messageList);
// Update UI
// Assume there is a refresh function refreshUI()
refreshUI();
}
function onPeerMessageReceived(
zim: ZIM,
messageList: ZIMMessage[],
info: ZIMMessageReceivedInfo,
fromUserID: string
) {
// Only process messages from the current conversation
if (fromUserID !== myConversationID) return;
// Merge messages into message list
addMessage(messageList);
// Update UI, such as refreshing message list
updateUI();
}
function onPeerMessageReceived(
zim: ZIM,
messageList: ZIMMessage[],
info: ZIMMessageReceivedInfo,
fromUserID: string
) {
// Only process messages from the current conversation
if (fromUserID !== myConversationID) return;
// Merge messages into message list
addMessage(messageList);
// Update UI, such as refreshing message list
updateUI();
}
void onPeerMessageReceived(
ZIM zim,
List<ZIMMessage> messageList,
ZIMMessageReceivedInfo info,
String fromUserID,
) {
// Only process messages from the current conversation
if (fromUserID != myConversationID) return;
// Merge messages into message list
addMessage(messageList);
// Update UI
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
if (myMessageList.isNotEmpty) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
void onPeerMessageReceived(
ZIM zim,
List<ZIMMessage> messageList,
ZIMMessageReceivedInfo info,
String fromUserID,
) {
// Only process messages from the current conversation
if (fromUserID != myConversationID) return;
// Merge messages into message list
addMessage(messageList);
// Update UI
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
if (myMessageList.isNotEmpty) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
Render Local Sent Messages to the Chat Interface
Listen to the messageSentStatusChanged interface to get the change of the sending status of the local sent message (sending, sending successfully, and sending failed), and render the local sent message and its sending status to the chat interface.
@Override
public void onMessageSentStatusChanged(List<ZIMMessageSentStatusChangeInfo> messageSentStatusChangeInfoList) {
for (ZIMMessageSentStatusChangeInfo info : messageSentStatusChangeInfoList) {
if (!info.getMessage().getConversationID().equals(myConversationID)) {
continue;
}
addMessage(Collections.singletonList(info.getMessage()));
}
// Update UI
runOnUiThread(() -> {
myAdapter.notifyDataSetChanged();
if (!myMessageList.isEmpty()) {
recyclerView.scrollToPosition(myMessageList.size() - 1);
}
});
}
@Override
public void onMessageSentStatusChanged(List<ZIMMessageSentStatusChangeInfo> messageSentStatusChangeInfoList) {
for (ZIMMessageSentStatusChangeInfo info : messageSentStatusChangeInfoList) {
if (!info.getMessage().getConversationID().equals(myConversationID)) {
continue;
}
addMessage(Collections.singletonList(info.getMessage()));
}
// Update UI
runOnUiThread(() -> {
myAdapter.notifyDataSetChanged();
if (!myMessageList.isEmpty()) {
recyclerView.scrollToPosition(myMessageList.size() - 1);
}
});
}
void onMessageSentStatusChanged(const std::vector<ZIMMessageSentStatusChangeInfo>& messageSentStatusChangeInfoList) {
for (const auto& info : messageSentStatusChangeInfoList) {
if (info.message.conversationID != myConversationID) continue;
addMessage({info.message}); // Assume addMessage supports vector or list
}
refreshUI(); // Custom refresh UI function
}
void onMessageSentStatusChanged(const std::vector<ZIMMessageSentStatusChangeInfo>& messageSentStatusChangeInfoList) {
for (const auto& info : messageSentStatusChangeInfoList) {
if (info.message.conversationID != myConversationID) continue;
addMessage({info.message}); // Assume addMessage supports vector or list
}
refreshUI(); // Custom refresh UI function
}
zim.onMessageSentStatusChanged = (messageSentStatusChangeInfoList: ZIMMessageSentStatusChangeInfo[]) => {
for (const info of messageSentStatusChangeInfoList) {
if (info.message.conversationID !== myConversationID) continue;
addMessage([info.message]);
}
updateUI(); // Custom refresh UI function
};
zim.onMessageSentStatusChanged = (messageSentStatusChangeInfoList: ZIMMessageSentStatusChangeInfo[]) => {
for (const info of messageSentStatusChangeInfoList) {
if (info.message.conversationID !== myConversationID) continue;
addMessage([info.message]);
}
updateUI(); // Custom refresh UI function
};
void onMessageSentStatusChanged(List<ZIMMessageSentStatusChangeInfo> messageSentStatusChangeInfoList) {
for (var info in messageSentStatusChangeInfoList) {
if (info.message.conversationID != myConversationID) continue;
addMessage([info.message]);
}
// Update UI
}
void onMessageSentStatusChanged(List<ZIMMessageSentStatusChangeInfo> messageSentStatusChangeInfoList) {
for (var info in messageSentStatusChangeInfoList) {
if (info.message.conversationID != myConversationID) continue;
addMessage([info.message]);
}
// Update UI
}
By following the above steps, you can render historical messages, real-time received messages, and local sent messages (including sending status) in the chat page.