Implementation of Message Read Receipts in Mobile Chat
Checkmarks in a messenger are not just UI. Behind them is state synchronization across devices, conflict resolution on unstable connections, and proper pagination history handling. Without proper architecture you get classic bugs: message shows "read" though user didn't see it, or status doesn't update after app restart.
Status Architecture
Typical model: each message has status — sent, delivered, read. Stored on server, synchronized via WebSocket or polling. Most common mistake — marking message as read on receipt (onMessage), not on actual visibility on screen.
Correct approach — track visibility via:
-
Android:
RecyclerView.OnScrollListener+LinearLayoutManager.findFirstCompletelyVisibleItemPosition()/findLastCompletelyVisibleItemPosition(). Mark as read only messages in visible area. -
iOS:
UITableView.indexPathsForVisibleRows+ delegate methodtableView(_:willDisplay:forRowAt:). -
Flutter:
VisibilityDetectorfromvisibility_detectorpackage or customScrollNotificationlistener.
Request batching is critical: don't send read for each message separately. Collect array of IDs and send one request with debounce 500–1000 ms after scroll stops.
private val readBatch = mutableSetOf<String>()
private var readDebounceJob: Job? = null
fun markVisible(messageIds: List<String>) {
readBatch.addAll(messageIds)
readDebounceJob?.cancel()
readDebounceJob = viewModelScope.launch {
delay(700)
if (readBatch.isNotEmpty()) {
sendReadReceipts(readBatch.toList())
readBatch.clear()
}
}
}
Display and Synchronization
Status indicators on sender side update via WebSocket event or Firebase listener. Important: in group chats "read" means "read by all" or "read by at least one" — this is a business decision that affects data schema. Telegram model stores read_by_count, WhatsApp model — read_by: [userId].
History loading via pagination creates separate problem: if user scrolls old messages, they shouldn't auto-mark as read — only current ones. Solved via isAtBottom flag and conditional visibility tracking launch.
Offline behavior. Statuses sent offline must cache locally (Room / CoreData / Hive) and send on connection restore. Otherwise user reads messages but sender never learns about it.
What's Included
Design status schema for your chat type (personal / group), implement visibility tracking, request batching, WebSocket updates on sender side, offline caching. Check correctness on edge cases: fast scroll, simultaneous open on multiple devices, connection breaks.
Timeline: 3–6 days depending on chat complexity and backend readiness.







