Skip to content

Conversation

@garthvh
Copy link
Member

@garthvh garthvh commented Oct 20, 2025

  • Message list performance improvements
  • Mesh Map Cleanup
  • Show who is relaying messages
  • Improved Manual TCP device connections
  • Defer saving of node list

garthvh and others added 10 commits October 17, 2025 18:11
* Remove extra want config call when adding a contact

* App badge and unnecessary notification fixes (#1455)

* - Fix for app badge not going to zero if a message arrives while you have that chat open
- Fix for push notifications popping up when a message is received while that chat is open

* Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message

* Fix: Channels help grammer fix (#1452)

* remove outdated TCP not available on Apple devices (#1450)

* Update initial onboarding view

* remove toggle gating for mac

* Update crash reporting opt out in real time

* Update onboarding text

* Use mDNS text records for node name

* TCP IP and port on the connection screen

* Hide app icon chooser on mac

* Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465)

* 2.7.5 Working Changes (#1460)

* Remove extra want config call when adding a contact

* App badge and unnecessary notification fixes (#1455)

* - Fix for app badge not going to zero if a message arrives while you have that chat open
- Fix for push notifications popping up when a message is received while that chat is open

* Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message

* Fix: Channels help grammer fix (#1452)

* remove outdated TCP not available on Apple devices (#1450)

* Update initial onboarding view

* remove toggle gating for mac

* Update crash reporting opt out in real time

* Update onboarding text

---------

Co-authored-by: Gnome Adrift <[email protected]>
Co-authored-by: Zain Kergaye <[email protected]>
Co-authored-by: NillRudd <[email protected]>

* UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise

* UserList: replace 5 slow calls to user.messageList with new fast calls

* NodeList: always put the connected node at the top of list (if it matches the node filters)

* ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N)

* ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls

* Fix incorrect appState.unreadDirectMessages calculations

* MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages

* UserMessageList: use @fetchrequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages

* Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension

* Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids)

* MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count

* Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild

* ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity

* ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification

* ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear

* ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active

---------

Co-authored-by: Garth Vander Houwen <[email protected]>
Co-authored-by: Gnome Adrift <[email protected]>
Co-authored-by: Zain Kergaye <[email protected]>
Co-authored-by: NillRudd <[email protected]>

* message-list-performance: revert scrolling changes (#1472)

* Revert e0f0b4a (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear)

* Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification"

This reverts commit ee1a7c4.

---------

Co-authored-by: Gnome Adrift <[email protected]>
Co-authored-by: Zain Kergaye <[email protected]>
Co-authored-by: NillRudd <[email protected]>
Co-authored-by: Jake-B <[email protected]>
Co-authored-by: Mike Robbins <[email protected]>
…d from #1424 (#1477)

* Fix: "Retrieving nodes" significantly slower after reconnect (#1424)

The node database retrieval was calling context.save() for every single
NodeInfo packet received (250 saves for 250 nodes). This caused severe
performance degradation on reconnect when CoreData had accumulated state.

Root Cause:
- nodeInfoPacket() called context.save() immediately for each node
- With 250 nodes, this meant 250 individual CoreData save operations
- On first connection, CoreData is fresh and fast
- On reconnect, CoreData has accumulated change tracking, undo management,
  and memory pressure, making each save progressively slower
- This resulted in 10+ second retrieval times vs 1-2 seconds initially

Solution:
- Added deferSave parameter to nodeInfoPacket() function
- During database retrieval (.retrievingDatabase state), defer all saves
- Perform a single batch save when database retrieval completes
  (when NONCE_ONLY_DB configCompleteID is received)
- This reduces 250 saves to 1 save

Performance Impact:
- Eliminates N individual saves during node database sync
- Reduces database retrieval time back to 1-2 seconds on reconnect
- Matches first-connection performance consistently

Fixes #1424

* Revert *MessageListUnified files

---------

Co-authored-by: Martin Bogomolni <[email protected]>
Co-authored-by: Jake-B <[email protected]>
…ckable at the highest zoom levels (#1478)

Co-authored-by: Garth Vander Houwen <[email protected]>
@garthvh garthvh requested a review from Copilot October 27, 2025 01:36
Copy link
Contributor

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 updates the app to version 2.7.6 with improvements focused on performance optimization, UI refinements, and message handling enhancements.

Key changes:

  • Optimized Core Data operations by introducing deferred saves during database retrieval and using count-based queries for unread messages
  • Refactored channel/message link handling to use a new SaveChannelLinkData struct for cleaner state management
  • Enhanced map visualization with fuzzing for overlapping nodes and precision circle indicators
  • Improved TCP device display by showing connection details (IP:Port) in the UI

Reviewed Changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
SaveChannelQRCode.swift Adds SaveChannelLinkData struct to support refactored channel link handling
AppSettings.swift Hides app icon picker on macCatalyst platform
DeviceOnboarding.swift Simplifies privacy notice text
NodeList.swift Sorts nodes to display connected device first
ShareContactQRDialog.swift Adds manuallyVerified field to shared contacts
MapSettingsForm.swift Removes redundant tap gesture handler for node history toggle
MeshMapContent.swift Implements precision-based coordinate fuzzing and adds precision circles for reduced-precision positions
UserMessageList.swift Optimizes message fetching with @FetchRequest and precomputed previous message lookup
UserList.swift Uses computed properties to avoid redundant message list access
MessageText.swift Refactors to use SaveChannelLinkData for channel link state management
ChannelMessageList.swift Implements same optimizations as UserMessageList
ChannelList.swift Uses computed properties for message checks
Connect.swift Displays IP:Port for TCP connections
MeshtasticApp.swift Disables Datadog analytics and uses SaveChannelLinkData for URL handling
MeshPackets.swift Adds deferSave parameter for batch processing and uses context-aware unread count methods
UserEntityExtension.swift Introduces efficient fetch request-based methods for messages and unread counts
PositionEntityExtension.swift Adds coordinate fuzzing logic for overlapping nodes
MyInfoEntityExtension.swift Implements efficient count-based unread message tracking
ChannelEntityExtension.swift Adds fetch request methods and optimized unread counting
TCPTransport.swift Parses service TXT records to extract node names and stores connection details
Device.swift Adds connectionDetails property for additional connection information
AccessoryManager.swift Implements batch save after database retrieval for performance
AccessoryManager+MQTT.swift Updates to use context-aware unread message methods
AccessoryManager+FromRadio.swift Defers saves during database retrieval
project.pbxproj Updates marketing version to 2.7.6
Localizable.xcstrings Updates localization strings for privacy notice

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

@garthvh garthvh requested a review from Copilot October 28, 2025 14:29
Copy link
Contributor

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

Copilot reviewed 40 out of 41 changed files in this pull request and generated 7 comments.


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

import OSLog

struct ShareContactQRDialog: View {
let manuallyVerified = false
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

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

The manuallyVerified constant is hardcoded to false. Consider making this a parameter or property if manual verification functionality will be added in the future, or add a comment explaining why it's always false.

Suggested change
let manuallyVerified = false
let manuallyVerified: Bool

Copilot uses AI. Check for mistakes.
let isCurrentUser: Bool
@Binding var isShowingDeleteConfirmation: Bool
let onReply: () -> Void
@State var relayDisplay: String? = nil
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

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

The relayDisplay state variable should be marked as private to encapsulate internal view state and prevent unintended external access.

Suggested change
@State var relayDisplay: String? = nil
@State private var relayDisplay: String? = nil

Copilot uses AI. Check for mistakes.
// Store the Data in UserDefaults
UserDefaults.standard.set(data, forKey: Keys.manualConnections.rawValue)
} catch {
print("Failed to encode manualConnections: \(error)")
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

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

Use Logger instead of print() for error logging to maintain consistency with the rest of the codebase and enable proper log level filtering.

Copilot uses AI. Check for mistakes.
* MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame

* MeshMapContent: factor out reducedPrecisionMapCircles into a separate function

* MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas)
* NodeMapContent: move Route Lines out of ForEach

* NodeMapContent: move Convex Hull out of ForEach

* NodeMapContent: Replace `position.nodePosition?` with `node`

* NodeMapContent: drop unnecessary LazyVStack in showNodeHistory

* NodeMapContent: hoist out nodeColorSwift

* Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively

* Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation

* NodeMapContent: remove unused @State

* NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops

* NodeMapSwiftUI: disable animation during SwiftUI transactions

* NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views

* NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations

* NodeMapContent: Shared coordinate list for Route Lines and Convex Hull

* NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16)
@garthvh garthvh requested a review from Copilot October 30, 2025 21:35
Copy link
Contributor

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

Copilot reviewed 48 out of 49 changed files in this pull request and generated 6 comments.


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

return false // First message will have no timestamp
}

func relayDisplay() -> String? {
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Extra whitespace before opening brace. Remove the extra space for consistency with Swift style guidelines.

Suggested change
func relayDisplay() -> String? {
func relayDisplay() -> String? {

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +75
guard self.relayNode != 0 else { return nil }
let context = PersistenceController.shared.container.viewContext

let relaySuffix = Int64(self.relayNode & 0xFF)
let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix)

do {
let users = try context.fetch(request)

// If exactly one match is found, return its name
if users.count == 1, let name = users.first?.longName, !name.isEmpty {
return "\(name)"
}

// If no exact match, find the node with the smallest hopsAway
if let closestNode = users.min(by: { lhs, rhs in
guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else {
return false
}
return lhsHops < rhsHops
}), let name = closestNode.longName, !name.isEmpty {
return "\(name)"
}

// Fallback to hex node number if no matches
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))

} catch {
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
}
}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation using spaces instead of tabs. Lines 44, 47-75 use spaces while the rest of the file uses tabs. Normalize to use tabs throughout for consistency.

Suggested change
guard self.relayNode != 0 else { return nil }
let context = PersistenceController.shared.container.viewContext
let relaySuffix = Int64(self.relayNode & 0xFF)
let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix)
do {
let users = try context.fetch(request)
// If exactly one match is found, return its name
if users.count == 1, let name = users.first?.longName, !name.isEmpty {
return "\(name)"
}
// If no exact match, find the node with the smallest hopsAway
if let closestNode = users.min(by: { lhs, rhs in
guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else {
return false
}
return lhsHops < rhsHops
}), let name = closestNode.longName, !name.isEmpty {
return "\(name)"
}
// Fallback to hex node number if no matches
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
} catch {
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
}
}
guard self.relayNode != 0 else { return nil }
let context = PersistenceController.shared.container.viewContext
let relaySuffix = Int64(self.relayNode & 0xFF)
let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix)
do {
let users = try context.fetch(request)
// If exactly one match is found, return its name
if users.count == 1, let name = users.first?.longName, !name.isEmpty {
return "\(name)"
}
// If no exact match, find the node with the smallest hopsAway
if let closestNode = users.min(by: { lhs, rhs in
guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else {
return false
}
return lhsHops < rhsHops
}), let name = closestNode.longName, !name.isEmpty {
return "\(name)"
}
// Fallback to hex node number if no matches
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
} catch {
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
}
}

Copilot uses AI. Check for mistakes.
// Store the Data in UserDefaults
UserDefaults.standard.set(data, forKey: Keys.manualConnections.rawValue)
} catch {
print("Failed to encode manualConnections: \(error)")
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Use Logger instead of print for consistent logging throughout the app. Replace with Logger.data.error(\"Failed to encode manualConnections: \\(error.localizedDescription, privacy: .public)\").

Copilot uses AI. Check for mistakes.
import Foundation

// Maintains an observable list of devices that's backed by UserDefaults
public class ManualConnectionList: ObservableObject {
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

The class is marked as public but is in an internal module. Change to final class ManualConnectionList: ObservableObject for better performance (final prevents subclassing and enables optimizations) and appropriate access control.

Suggested change
public class ManualConnectionList: ObservableObject {
final class ManualConnectionList: ObservableObject {

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +92
if let nodeId = txtRecords["id"], nodeId.count > 4 {
if nodeNameString.count > 0 {
nodeNameString += "_"
}
nodeNameString += String(decoding: nodeId.suffix(4), as: UTF8.self)
}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Magic number 4 is used without explanation. Add a comment or named constant to clarify why the last 4 characters of the node ID are used, e.g., let nodeIdSuffixLength = 4 // Last 4 hex digits of node ID.

Copilot uses AI. Check for mistakes.
Comment on lines +571 to +574
VStack (alignment: .leading) {
Text("Last seen device:")
Text("\(String(describing: device))")
}
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

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

Using String(describing:) on a type that already conforms to CustomStringConvertible is redundant. Simplify to Text(device.description) or use string interpolation directly: Text(\"\\(device)\").

Copilot uses AI. Check for mistakes.
@garthvh garthvh requested a review from Copilot November 1, 2025 21:54
Copy link
Contributor

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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.

5 participants