Summary
Messages are currently ordered (in every query in lib/database/daos/messages_dao.dart -- at least 14 separate call sites, covering channel messages, DMs, and likely other message-derived views) strictly by Messages.timestamp, which is the sender's own device clock value embedded in the message payload at compose time (the first 4 bytes of the over-the-air plaintext, before encryption). This value is never validated or bounded.
Concrete proof this is broken
In a live Public channel on our test setup, a device ("Connor123ttt") sent several messages with timestamps of 2093-06-06 -- a broken clock, 67 years in the future. Since watchMessagesByChannelForCompanion (the query the chat screen uses) sorts ascending by this untrusted field, those messages sorted to the very bottom of the list -- exactly where the UI auto-scrolls to, expecting "most recent." This buried genuinely recent messages (e.g. one from ~10 minutes prior with a correct 2026-07-19 timestamp) further up the scroll, making them easy to miss entirely.
Any single device with a wrong clock -- broken, unset, intentionally spoofed, or just badly drifted -- can corrupt the perceived message order for everyone in a shared channel. This isn't a sync bug; the messages are received and stored correctly. It's purely a display/ordering bug.
Desired behavior
- Messages must sort in strict, monotonic local receive order -- i.e. the order this device actually received them, immune to any sender's clock.
- Add a
receivedAt column to Messages, set once at insert time from DateTime.now() (the device running the app's own clock), and use it (not the existing timestamp column) for all ordering in messages_dao.dart.
receivedAt should also become the primary timestamp displayed on message bubbles (currently channel_chat_screen.dart/direct_message_screen.dart show Messages.timestamp via formatMessageTime).
- Keep the existing sender-embedded
timestamp field as-is -- don't discard it. It's still useful (e.g. to show "sent" vs "received" if they diverge meaningfully, or for diagnostics) as a secondary/reference value, just not for ordering or as the primary displayed time.
Scope note
This touches a schema migration (new column, all-messages backfill isn't really meaningful since we don't know true historical receive order for already-stored rows -- likely just default backfilled rows to their existing timestamp or insertion rowid order, whichever is simpler, since this only matters going forward for correctness), the DAO query layer (many call sites), and the two chat screens' display of message time.
Summary
Messages are currently ordered (in every query in
lib/database/daos/messages_dao.dart-- at least 14 separate call sites, covering channel messages, DMs, and likely other message-derived views) strictly byMessages.timestamp, which is the sender's own device clock value embedded in the message payload at compose time (the first 4 bytes of the over-the-air plaintext, before encryption). This value is never validated or bounded.Concrete proof this is broken
In a live Public channel on our test setup, a device ("Connor123ttt") sent several messages with timestamps of
2093-06-06-- a broken clock, 67 years in the future. SincewatchMessagesByChannelForCompanion(the query the chat screen uses) sorts ascending by this untrusted field, those messages sorted to the very bottom of the list -- exactly where the UI auto-scrolls to, expecting "most recent." This buried genuinely recent messages (e.g. one from ~10 minutes prior with a correct2026-07-19timestamp) further up the scroll, making them easy to miss entirely.Any single device with a wrong clock -- broken, unset, intentionally spoofed, or just badly drifted -- can corrupt the perceived message order for everyone in a shared channel. This isn't a sync bug; the messages are received and stored correctly. It's purely a display/ordering bug.
Desired behavior
receivedAtcolumn toMessages, set once at insert time fromDateTime.now()(the device running the app's own clock), and use it (not the existingtimestampcolumn) for all ordering inmessages_dao.dart.receivedAtshould also become the primary timestamp displayed on message bubbles (currentlychannel_chat_screen.dart/direct_message_screen.dartshowMessages.timestampviaformatMessageTime).timestampfield as-is -- don't discard it. It's still useful (e.g. to show "sent" vs "received" if they diverge meaningfully, or for diagnostics) as a secondary/reference value, just not for ordering or as the primary displayed time.Scope note
This touches a schema migration (new column, all-messages backfill isn't really meaningful since we don't know true historical receive order for already-stored rows -- likely just default backfilled rows to their existing
timestampor insertion rowid order, whichever is simpler, since this only matters going forward for correctness), the DAO query layer (many call sites), and the two chat screens' display of message time.