fix: resolve recents number collisions to the external contact (WT-1024)#1371
Conversation
The recents list and the contact opened from it ("View Contact") joined a
call-log number to a contact with no source tie-break, so when a number was
shared by a local and an external (PBX) contact the winner was left to SQLite's
unspecified row order (the first row kept by _rowsToRecent). That let the list
and the opened contact card show a different source than the call screen, which
reads as mixed name/avatar.
watchLastRecents and getRecentByCallId now order the joined rows by
contactsTable.sourcePriorityOrder() so the external contact is kept
deterministically. watchLastRecents keeps createdAt/hungUpAt as the primary
ordering (orderBy on a joined statement replaces the term list, so the full list
is passed) and applies the source priority only as a per-call-log tie-break.
Covered by recents_dao_test (both insertion orders, list order preserved,
non-colliding numbers unaffected).
There was a problem hiding this comment.
Pull request overview
Resolves WT-1024 by making Recents contact resolution deterministic when a call-log number matches both a local device contact and an external (PBX) contact, ensuring the external contact consistently wins.
Changes:
- Add source-priority ordering to the Recents contact join to deterministically pick the external contact on number collisions.
- Preserve recents newest-first ordering while applying source priority only as a per-call-log tie-break.
- Add DAO tests covering collision resolution (both insertion orders), list ordering, and non-collision behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/data/app_database/lib/src/daos/recents_dao.dart | Adds source-priority ordering to joined recents queries to resolve local-vs-external collisions deterministically. |
| packages/data/app_database/test/recents_dao_test.dart | Introduces regression tests validating deterministic external-first behavior and preserving list order. |
| final callsQuery = select(callLogsTable)..where((t) => t.createdAt.isBiggerOrEqualValue(clock.agoBy(period))); | ||
|
|
||
| final sourcePhone = alias(contactPhonesTable, 'source_phone'); | ||
| final contactPhones = alias(contactPhonesTable, 'contact_phones'); |
There was a problem hiding this comment.
Good catch, but this is pre-existing and out of scope for this PR. The change here only adds the source-priority ordering for the resolved contact (contactData); it does not touch the contact-phones plumbing.
The alias vs non-aliased mismatch (the joined contact_phones alias is unused by _rowsToRecent, which reads the base contactPhonesTable, and the presence join is also on the base table) is already on develop. I verified empirically that recents contact phones still come back populated and the read does not throw - a recent for a contact with phone "7" returns phones=[7] - because drift pulls the base table in via the presence ON-clause. So nothing is missing in the common case; the latent risk is a cross-join when a contact has multiple phones.
Aligning recents on the alias (like favorites_dao) also affects getRecentByCallId, which joins the base contactPhonesTable, and their shared _rowsToRecent, so I'm tracking it as a separate cleanup rather than widening this PR.
Overview
Fixes the WT-1024 surface: in the Recents list (and the contact opened from it via
"View Contact"), a phone number shared by a local device contact and an external
(PBX) contact resolved to an arbitrary source, so the list/contact card could show
a different identity than the call screen - read as mixed name and avatar.
recents_daojoined the call-log number to a contact with no source tie-break, sothe winner was left to SQLite's unspecified row order (the first row kept by
_rowsToRecent). This builds on the centralized policy from #1369.Changes
watchLastRecentsandgetRecentByCallIdnow order the joined rows bycontactsTable.sourcePriorityOrder()(external/PBX first), so_rowsToRecentkeeps the external contact deterministically.
watchLastRecentskeepscreatedAt/hungUpAtas the primary ordering and addsthe source priority only as a per-call-log tie-break. Note:
orderByon a joinedstatement replaces the term list (it does not append), so the full ordering is
passed explicitly - otherwise the newest-first list order would be lost.
resolved contact, so a deterministic pick fixes both at once.
Testing
recents_dao_test.dart: external wins on collision for bothwatchLastRecentsandgetRecentByCallId, both insertion orders; the list keepsnewest-first order; non-colliding numbers resolve to their single contact.
app_databasesuite green (281 tests);dart analyzeclean.Follow-up
The same collision exists in voicemail (
voicemail_dao), which additionallyduplicates a voicemail row on a sender collision. Tracked separately; not part of
this PR.