feat: add 'Locate on Map' to Chats, Messaging, and Contacts screens#597
Conversation
Greptile SummaryThis PR adds "Locate on Map" functionality from three entry points (ChatsScreen long-press menu, MessagingScreen top-bar icon, ContactsScreen long-press menu), wiring them through a shared Key changes:
Issues identified:
Confidence Score: 4/5
Last reviewed commit: 8c5a9fe |
Additional Comments (3)
As a result, clicking "Locate on Map" from any of the three entry points will navigate the user to the Map tab correctly, but the camera will never animate to the contact's marker. The
val pendingFocus by viewModel.pendingFocusContact.collectAsState()
LaunchedEffect(pendingFocus) {
pendingFocus?.let { hash ->
val marker = state.contactMarkers.find {
it.destinationHash.equals(hash, ignoreCase = true)
}
marker?.let {
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(LatLng(it.latitude, it.longitude), 15f)
)
}
viewModel.consumePendingFocus()
}
}Without this, the core user-visible behaviour of the feature (focusing the map on the contact) is missing.
The key for the If the motivation was to avoid a duplicate-key crash when two contacts share the same destination hash (with different casing), a simpler fix that keeps stable keys would be: The
Resetting the value on close prevents this race: The same pattern applies to |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
36b2e9f to
9d43716
Compare
|
@greptile |
data/src/main/java/com/lxmf/messenger/data/db/dao/ReceivedLocationDao.kt
Show resolved
Hide resolved
Additional Comments (2)
The race condition occurs because:
Fix: Only consume the focus after a successful animation: marker?.let {
val cameraPosition = CameraPosition.Builder()
.target(LatLng(it.latitude, it.longitude))
.zoom(15.0)
.build()
map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
viewModel.consumePendingFocus() // ← move inside let block
}Alternatively, add
Meanwhile, the map's Fix: Add an expiry check to the mapping: receivedLocationDao.observeLatestLocationForSender(hash)
.map { it != null && (it.expiresAt == null || it.expiresAt > System.currentTimeMillis()) }The same expiry mismatch exists in |
Add the ability to locate a contact on the map from multiple entry points: - ChatsScreen: 'Locate on Map' in conversation long-press context menu - MessagingScreen: Map icon button in top bar when contact has known location - ContactsScreen: 'Locate on Map' in contact long-press context menu Implementation: - MapViewModel: shared instance with pendingFocusContact mechanism to animate camera to existing contact marker without creating duplicates - ReceivedLocationDao: add observeLatestLocationForSender() and getLatestLocationForSender() queries - ChatsViewModel, ContactsViewModel: inject ReceivedLocationDao, add getContactLocation() suspend method - MessagingViewModel: inject ReceivedLocationDao, reactive hasContactLocation flow - MainActivity: wire callbacks to focusOnContact() + navigate to Map tab - deduplicateContactMarkersByDestination() utility for case-insensitive dedup Tests: - MapViewModelTest: pendingFocusContact lifecycle (set, overwrite, consume) - ChatsViewModelTest: getContactLocation with/without location data - ContactsViewModelTest: getContactLocation with/without location data - MessagingScreenTest: hasContactLocation mock for UI test - Updated ViewModel test constructors for new ReceivedLocationDao dependency
9d43716 to
68215cf
Compare
- MapScreen: consume pendingFocus only after successful animation, add contactMarkers as LaunchedEffect key for retry on late load - ChatsViewModel, ContactsViewModel: filter expired locations in getContactLocation() (expiresAt check) - MessagingViewModel: filter expired locations in hasContactLocation StateFlow - ChatsScreen, ContactsScreen: clear stale location state on menu close - ContactsScreen: use stable lowercase key instead of index-based key - LocationSharingManager: normalize senderHash to lowercase defensively - Tests: add expired location test cases for getContactLocation()
|
@greptile |
Additional Comments (5)
The same issue exists in
The same root cause affects
If the contact's marker has been removed from Consider adding a timeout or consuming the pending focus after a maximum number of retries (or when the contact is confirmed absent from markers): marker?.let {
val cameraPosition = CameraPosition.Builder()
.target(LatLng(it.latitude, it.longitude))
.zoom(15.0)
.build()
map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
viewModel.consumePendingFocus()
} ?: run {
// If markers have loaded and the hash still isn't found, stop waiting
if (state.contactMarkers.isNotEmpty() && !state.isLoading) {
viewModel.consumePendingFocus()
}
}
The same pattern exists in:
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The method body in Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
- Normalize hash to lowercase on DAO lookups (ChatsViewModel, ContactsViewModel, MessagingViewModel) for defensive consistency - Consume pendingFocus only after successful camera animation; fall back and consume if markers loaded but contact not found - Extract hardcoded 'Locate on Map' strings to strings.xml - Add expired location regression tests
…Repository Move getContactLocation and observeHasLocation logic (hash normalization + expiry filtering) from ChatsViewModel, ContactsViewModel and MessagingViewModel into a shared ReceivedLocationRepository in the data module. ViewModels now delegate to the repository. Add ReceivedLocationRepositoryTest covering coordinates, null, expiry and case-normalization scenarios for both suspend and Flow APIs.
|
@greptile |
Additional Comments (4)
The same race can happen in the menu-open path via Consider adding a periodic ticker (e.g.
The same gap exists in Add a default stub in the setup to make the mock safe for all tests:
Add a default stub in the setup to make the mock safe for all tests:
The index introduced by switching to |
- Add default mock stub for observeHasLocation(any()) in MessagingViewModelTest and MessagingViewModelImageLoadingTest to prevent strict mock failures. - Revert unnecessary itemsIndexed back to items in ContactsScreen (index was unused).
Additional Comments (3)
This means the "Locate on Map" icon may remain visible in the top bar until the next database write (e.g., when Consider combining the expiry check with a time-based re-evaluation mechanism (e.g., a
Consider aligning the logic by either:
This would improve UX consistency and clarify intent to users. |
- ReceivedLocationRepository now includes a 1-hour grace period matching MapViewModel.GRACE_PERIOD_MS, so "Locate on Map" remains available as long as the marker is visible on the map. - Fix orphaned KDoc block: move deleteMarker() documentation to the correct position in MapViewModel. - Update ReceivedLocationRepositoryTest with grace period scenarios.
Summary
Add the ability to locate a contact on the map from multiple entry points in the app.
New Feature: Locate on Map
Entry points
Implementation
ColumbaNavigationlevel withpendingFocusContactmechanism —focusOnContact(destinationHash)/consumePendingFocus()— to animate camera to existing contact marker without creating duplicatesobserveLatestLocationForSender()reactive Flow query andgetLatestLocationForSender()suspend queryReceivedLocationDao, addgetContactLocation()suspend methodReceivedLocationDao, reactivehasContactLocationStateFlowfocusOnContact()+ navigate to Map tab usingpopUpTo+launchSingleTop+restoreStatededuplicateContactMarkersByDestination(): case-insensitive deduplication utility for contact markersTests
pendingFocusContactlifecycle (initial null, set, overwrite, consume/clear)getContactLocation()with and without location datagetContactLocation()with and without location datahasContactLocationmock for UI testReceivedLocationDaodependencyManual Testing