Skip to content

feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified#281

Draft
JCBird1012 wants to merge 1 commit into
Avi0n:devfrom
JCBird1012:lastHeard
Draft

feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified#281
JCBird1012 wants to merge 1 commit into
Avi0n:devfrom
JCBird1012:lastHeard

Conversation

@JCBird1012
Copy link
Copy Markdown

@JCBird1012 JCBird1012 commented Apr 17, 2026

This pull request introduces a dedicated "Last Heard" timestamp tracking when the app actually confirmed a node is alive over radio, and clarifies the existing "Last Advert" field as the device-reported time ("Node Time Sent"). This clarification was chatted about briefly in the MeshCore Discord thread for MeshCore One.

  • Added lastHeardTimestamp to the Contact model and ContactDTO, set only on actual radio confirmation events (otherwise things like Reset Path would update lastModified and further mislead the user into believing the node was heard recently):

    • Advertisement receipt (known, unknown, and deferred contacts)
    • Path discovery responses
    • Ping responses
    • Incoming direct messages
  • Added effectiveLastHeard computed property that falls back to lastModified when lastHeardTimestamp is unset - this avoids existing contacts showing "Dec 31, 1969" during the transition until their first radio event populates the new field.

  • Added "Last Heard" row in contact detail views (ContactDetailView, ContactDetailSheet).

  • Renamed "Last Advert" label to "Node Time Sent" to clarify it's the device-reported timestamp.

  • Contact list rows now show effectiveLastHeard instead of lastModified (ContactRowView.swift).

  • Sort-by-last-heard now uses effectiveLastHeard (ContactsViewModel.swift).

IMG_4070

A repeater with its clock set correctly. Last Heard matches Node Time Sent.

IMG_4071

A repeater with its clock set incorrectly. Node Time Sent shows the repeater's wrong timestamp. Last Heard shows our local (more correct) time when we actually received the advert.

…lback to lastModified

Add dedicated lastHeardTimestamp field for radio reception tracking

Replace reliance on lastModified (which updates on sync, path changes,
and other non-radio events) with a dedicated lastHeardTimestamp that is
only set when we actually confirm a node is alive over the radio:

- Advertisement receipt (known, unknown, and deferred contacts)
- Path discovery responses
- Ping responses
- Incoming direct messages

Add effectiveLastHeard computed property on ContactDTO that falls back
to lastModified when lastHeardTimestamp is unset, ensuring existing
contacts display reasonable timestamps during the transition.

Rename "Last Advert" label to "Node Time Sent" and add new "Last Heard"
row in ContactDetailView and ContactDetailSheet. Update ContactRowView
and sort-by-last-heard to use effectiveLastHeard.

Updated locales with the changes.
Copilot AI review requested due to automatic review settings April 17, 2026 15:38
@JCBird1012 JCBird1012 changed the title feat(contacts): Add dedicated lastHeardTimestamp for nodes - with falllback to lastModified feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified Apr 17, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a dedicated “Last Heard” timestamp to track when the app actually confirmed a node is alive over radio, while clarifying the existing “Last Advert” timestamp as the node/device-reported time (“Node Time Sent”). It updates persistence, service event handling, sorting/display logic, and localized UI strings accordingly.

Changes:

  • Add lastHeardTimestamp to Contact/ContactDTO, plus effectiveLastHeard fallback behavior.
  • Record last-heard updates on radio confirmation events (adverts, path discovery responses, ping responses, direct messages).
  • Update UI to display “Last Heard”, rename “Last Advert” label to “Node Time Sent”, and update list sorting/display to use effectiveLastHeard.

Reviewed changes

Copilot reviewed 32 out of 33 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
MC1Tests/ViewModels/LineOfSightViewModelTests.swift Update test mock PersistenceStoreProtocol conformance with new API.
MC1Tests/ViewModels/ChatViewModelPaginationTests.swift Update test data store mock to include new last-heard API.
MC1Tests/Services/LinkPreviewCacheTests.swift Update mock persistence store protocol surface for new API.
MC1Services/Tests/MC1ServicesTests/Mocks/MockPersistenceStore.swift Implement updateContactLastHeard and preserve new DTO field across mock mutations.
MC1Services/Tests/MC1ServicesTests/Helpers/ContactDTO+Testing.swift Extend test factory helper to set lastHeardTimestamp.
MC1Services/Sources/MC1Services/SyncCoordinator+MessageHandlers.swift Update DM receive path to record “last heard”.
MC1Services/Sources/MC1Services/Services/PersistenceStore+Contacts.swift Persist lastHeardTimestamp and add updateContactLastHeard implementation.
MC1Services/Sources/MC1Services/Services/AdvertisementService.swift Record “last heard” on advert receipt and path discovery response.
MC1Services/Sources/MC1Services/Protocols/PersistenceStoreProtocol.swift Add updateContactLastHeard to the persistence protocol.
MC1Services/Sources/MC1Services/Models/Contact.swift Add model/DTO field + effectiveLastHeard computed property.
MC1/Views/Map/ContactDetailSheet.swift Display “Last Heard” row in map contact detail sheet.
MC1/Views/Contacts/ContactsViewModel.swift Sort by .lastHeard using effectiveLastHeard.
MC1/Views/Contacts/ContactRowView.swift Show effectiveLastHeard in contact list rows.
MC1/Views/Contacts/ContactDetailView.swift Display “Last Heard”, rename “Last Advert” label meaning, and update ping flow to record last-heard.
MC1/Resources/Localization/zh-Hans.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/zh-Hans.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/uk.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/uk.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/ru.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/ru.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/pl.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/pl.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/nl.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/nl.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/fr.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/fr.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/es.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/es.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/en.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/en.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/de.lproj/Map.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Localization/de.lproj/Contacts.strings Update “Last Advert” label meaning + add “Last Heard” localization.
MC1/Resources/Generated/L10n.swift Regenerate SwiftGen output for new/updated localization keys.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

RelativeTimestampText(timestamp: contact.lastModified)
RelativeTimestampText(timestamp: contact.effectiveLastHeard)
Comment on lines 286 to +290
)
_ = try await dataStore.saveContact(deviceID: deviceID, from: frame)
let contactID = try await dataStore.saveContact(deviceID: deviceID, from: frame)

// Mark the actual time we heard this node over radio
try? await dataStore.updateContactLastHeard(contactID: contactID, timestamp: timestamp)
Comment on lines +289 to +290
// Mark the actual time we heard this node over radio
try? await dataStore.updateContactLastHeard(contactID: contactID, timestamp: timestamp)
Comment on lines +180 to +183
try? await services.dataStore.updateContactLastHeard(
contactID: contactID,
timestamp: UInt32(Date().timeIntervalSince1970)
)
Comment on lines +446 to +449
try? await appState.services?.dataStore.updateContactLastHeard(
contactID: currentContact.id,
timestamp: UInt32(Date().timeIntervalSince1970)
)
switch order {
case .lastHeard:
return contacts.sorted { $0.lastModified > $1.lastModified }
return contacts.sorted { $0.effectiveLastHeard > $1.effectiveLastHeard }
/// Last modification timestamp (for sync watermarking)
public var lastModified: UInt32

/// Timestamp when we actually received an advertisement from this node (our local clock)
Comment on lines +778 to +781
HStack {
Text(L10n.Contacts.Contacts.Detail.lastHeard)
Spacer()
ConversationTimestamp(date: Date(timeIntervalSince1970: TimeInterval(currentContact.effectiveLastHeard)), font: .body)
Comment on lines +65 to +66
LabeledContent(L10n.Map.Map.Detail.lastHeard) {
ConversationTimestamp(date: Date(timeIntervalSince1970: TimeInterval(contact.effectiveLastHeard)), font: .body)
@JCBird1012 JCBird1012 changed the title feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified Draft: feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified Apr 17, 2026
@JCBird1012 JCBird1012 marked this pull request as draft April 17, 2026 15:48
@JCBird1012 JCBird1012 changed the title Draft: feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified feat(contacts): Add dedicated lastHeardTimestamp for nodes - with fallback to lastModified Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants