Skip to content

Show overlaps in public keys of repeaters#218

Merged
zjs81 merged 6 commits into
mainfrom
dev-mapOverlap
Mar 24, 2026
Merged

Show overlaps in public keys of repeaters#218
zjs81 merged 6 commits into
mainfrom
dev-mapOverlap

Conversation

@wel97459

Copy link
Copy Markdown
Collaborator

Show overlaps in public keys of repeaters on the map.
Added a function to handle filtering markers placed on the map.

Copilot AI review requested due to automatic review settings February 23, 2026 00:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adds functionality to visualize overlaps in public keys of repeaters on the map, where overlaps are detected by comparing the first byte of public keys. The feature helps identify potential routing conflicts when multiple repeaters/rooms share the same first byte in their public key. Additionally, the PR includes UI improvements to the path trace functionality, converting text buttons to icon buttons and adding a separate "return path" option.

Changes:

  • Added mapShowOverlaps setting to show only nodes with overlapping public key first bytes
  • Refactored marker filtering logic into a dedicated _filterContactsBySettings method
  • Enhanced path trace UI with icon buttons and separate forward/return path options

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/services/app_settings_service.dart Added setMapShowOverlaps method to update the new overlap display setting
lib/screens/map_screen.dart Core changes: new filtering logic, overlap detection, updated path trace UI with icon buttons, and overlap visualization (red markers with key prefix labels)
lib/models/app_settings.dart Added mapShowOverlaps boolean field with serialization support
lib/l10n/app_*.arb Added localized strings for "Repeater Key Overlaps" across all 15 supported languages
lib/l10n/app_localizations*.dart Generated localization code for the new string key

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

Comment thread lib/screens/map_screen.dart
Comment on lines +550 to +552
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

The condition checks if both contacts are either advTypeRepeater OR advTypeRoom. However, based on the PR title "Show overlaps in public keys of repeaters" and the English localization "Repeater Key Overlaps", this feature appears to be specifically for repeaters. Including rooms in the overlap detection may be intentional, but it's inconsistent with the user-facing messaging which only mentions repeaters. Consider either:

  1. Updating the UI text to mention both repeaters and rooms
  2. Restricting the check to only repeaters if rooms were included by mistake

Copilot uses AI. Check for mistakes.
Comment on lines +545 to +554
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

This code has O(n²) time complexity because the overlap detection runs a full scan of the contacts list for every contact being filtered. For large contact lists, this could cause noticeable performance degradation.

Consider optimizing by:

  1. Pre-computing overlaps once before the filtering loop
  2. Building a map of first-byte to contacts for O(1) lookup
  3. Or moving the overlap computation outside the filter function and storing it as metadata on contacts

Copilot uses AI. Check for mistakes.
Comment thread lib/screens/map_screen.dart Outdated
Comment on lines +1756 to +1783
tooltip: "Path Trace",
icon: const Icon(Icons.arrow_forward_outlined),
),
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
flipPathRound: true,
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
tooltip: "Build Return Path",
icon: const Icon(Icons.replay),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
tooltip: "Remove Last Point",
icon: const Icon(Icons.delete),

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

The tooltip strings "Path Trace", "Build Return Path", and "Remove Last Point" are hardcoded in English instead of using localization. This is inconsistent with the rest of the codebase which uses context.l10n for all user-facing strings. These should use localized strings from the l10n files.

Copilot uses AI. Check for mistakes.
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
// const SizedBox(height: 6),

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

This line is commented out but left in the code. If this spacing adjustment is no longer needed due to the button changes, the commented line should be removed entirely to keep the code clean. If it might be needed later, add a comment explaining why it's kept.

Copilot uses AI. Check for mistakes.
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

The code accesses publicKey.first without checking if the Uint8List is non-empty. While publicKey is a required non-nullable field, an empty Uint8List could still be passed, which would cause a runtime error when accessing .first. Consider adding a safety check or documenting that publicKey must never be empty.

Copilot uses AI. Check for mistakes.
Comment on lines +621 to +622
label: settings.mapShowOverlaps && !_isBuildingPathTrace
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

The code calls substring(0, 2) on publicKeyHex without checking its length. If publicKeyHex is shorter than 2 characters, this will throw a RangeError. While public keys are typically of a fixed length, consider adding a safety check or using a safer substring method like substring(0, min(2, publicKeyHex.length)).

Copilot uses AI. Check for mistakes.
Comment on lines +520 to +565
List<Contact> filtered = [];
bool addContact = false;
for (final contact in contacts) {
addContact = false;
if (!contact.hasLocation) continue;

// Apply node type filters
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
(settings.mapShowRepeaters ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
continue;
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
addContact = true;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
continue;
(settings.mapShowOtherNodes ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}

final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;

if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}

if (addContact) {
filtered.add(contact);
}
}

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

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

The filtering logic has several design issues:

  1. When mapShowOverlaps is enabled, it overrides individual node type filters by including repeaters and other nodes even if those filters are disabled (lines 530, 541). This means users will see repeaters/other nodes even if they've explicitly disabled those checkboxes in the UI. This creates a confusing user experience.

  2. The addContact boolean is set by multiple independent conditions, making the logic hard to follow. A contact is added if ANY of the node type conditions are true, which isn't immediately clear from the structure.

  3. The overlap detection at lines 556-559 acts as an exclusive filter - when enabled with no overlap found, it overrides the earlier addContact = true decisions. This means mapShowOverlaps acts as "show ONLY overlaps" rather than "highlight overlaps".

Consider refactoring to make the intent clearer, perhaps separating "which nodes to consider" from "which nodes to display" logic. Also consider whether overriding individual node type filters is the desired UX.

Copilot uses AI. Check for mistakes.
@zjs81

zjs81 commented Mar 6, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eed732ef64

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +278 to +280
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard path trace start when self location is unavailable

The radar action now force-unwraps connector.selfLatitude and connector.selfLongitude, but those fields are nullable in this screen (there are nearby null checks before rendering self-position markers). If the user opens the map before self info is populated, or if the device has no valid self location, tapping path trace will throw on the ! and crash this flow. Please gate this button/action on non-null coordinates or keep the previous start-without-self behavior.

Useful? React with 👍 / 👎.

Comment thread lib/screens/map_screen.dart Outdated
Comment on lines +495 to +496
if (!_isBuildingPathTrace)
_buildLegend(
contactsWithLocation,
settings,
sharedMarkers.length,
),
_buildLegend(filteredByTime, settings, sharedMarkers.length),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep legend counts consistent with key-prefix filtering

The legend is now built from filteredByTime, while markers are built from filteredByKeyPrefix, so enabling key-prefix filtering hides nodes on the map but the legend still counts them. In practice this makes the node totals inaccurate whenever mapKeyPrefixEnabled is on and can report nodes that are intentionally filtered out from the current view.

Useful? React with 👍 / 👎.

* feat: Enhance privacy settings and telemetry

- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.

* feat: Refactor repeater resolution logic across multiple screens
Copilot AI review requested due to automatic review settings March 21, 2026 16:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 51 out of 51 changed files in this pull request and generated 9 comments.


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

@@ -1,3 +1,4 @@
import 'dart:collection';

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

dart:collection is imported but doesn't appear to be used anywhere in this file. With flutter_lints, this will be flagged as an unused import and can fail flutter analyze.

Remove the import or use the specific collection type you intended to add.

Copilot uses AI. Check for mistakes.
Comment on lines +661 to 676
double lat = 0, lon = 0, weight = 1.0;
int counted = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
if (counted == 0) {
lat = a.latitude;
lon = a.longitude;
} else {
lat += a.latitude * weight;
lon += a.longitude * weight;
}
// weight subsequent anchors less to create a bias towards the first (if more than 2)
weight = weight / 2;
counted++;
}
position = _offsetGuessedPosition(
LatLng(lat / anchors.length, lon / anchors.length),

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

The new weighted anchor averaging logic divides by anchors.length, but the accumulated lat/lon values are no longer a simple sum (they include weights and treat the first anchor differently). This produces a biased value that is also incorrectly scaled (it will drift toward 0,0 as anchors increase).

If you want a weighted average, track weightSum and divide by that (and apply the intended weight to the first anchor as well).

Copilot uses AI. Check for mistakes.
Comment on lines +569 to +575
void clearAllHistories() {
_cache.clear();
_cacheAccessOrder.clear();
_autoRotationIndex.clear();
_floodStats.clear();
_storage.clearAllPathHistories();
_version = 0;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

clearAllHistories() calls _storage.clearAllPathHistories() which returns a Future<void>, but the future isn't awaited or wrapped in unawaited(...). With flutter_lints this is typically flagged by unawaited_futures, and it also means any errors during preference clearing will be dropped.

Make clearAllHistories async and await the call (or explicitly wrap it in unawaited(...) if fire-and-forget is intended).

Copilot uses AI. Check for mistakes.
Comment on lines +314 to +320
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: Text("Delete All Paths"),
subtitle: Text(
"Clear all path data from contacts.",
style: TextStyle(color: Colors.red[700]),
),
onTap: () => connector.deleteAllPaths(),

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

This settings tile introduces hard-coded English strings (title/subtitle). The rest of the screen uses l10n, so this will be untranslated in all locales.

Also, this performs a destructive action without any confirmation dialog. Consider using localized strings and showing a confirm dialog (similar to reboot).

Copilot uses AI. Check for mistakes.
Comment thread lib/l10n/app_bg.arb
Comment on lines 1893 to 1901
"@path_routeWeight": {
"placeholders": {
"weight": {
"value": {
"type": "String"
},
"max": {
"type": "String"
}
}

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

The @path_routeWeight metadata placeholders don't match the message template. The string uses {weight}/{max}, but the placeholders define value and max. This can break localization code generation/runtime formatting.

Rename the placeholder key to weight (to match {weight}) or update the message template to use {value} consistently.

Copilot uses AI. Check for mistakes.
Comment on lines 331 to +335
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

The Path Trace (radar) button unconditionally force-unwraps connector.selfLatitude/selfLongitude when starting a trace. If the device location isn't set yet, this will throw at runtime.

Disable the button until both values are non-null, or pass a fallback position (e.g., current map center).

Copilot uses AI. Check for mistakes.
Comment on lines +873 to +883
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

_filterContactsBySettings checks for overlaps by scanning the full contacts list (where(...).firstOrNull) for every contact, which is O(n²). On larger contact lists this can noticeably slow marker building (and it runs every rebuild).

Consider precomputing a map of firstByte -> count (or a set of overlapping first-bytes) once per build and using that for O(1) overlap checks.

Copilot uses AI. Check for mistakes.
Comment on lines +315 to 321
title: Text("Delete All Paths"),
subtitle: Text(
"Clear all path data from contacts.",
style: TextStyle(color: Colors.red[700]),
),
onTap: () => connector.deleteAllPaths(),
),

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

The UI copy says "Clear all path data from contacts.", but connector.deleteAllPaths() only clears local PathHistoryService cache/storage (it doesn't reset contact/device path overrides). This makes the action's effect unclear to users.

Update the subtitle to accurately describe what will be cleared, or extend the action to clear the additional path state it claims to remove.

Copilot uses AI. Check for mistakes.
Comment on lines +1143 to +1151
onPressed: () {
connector.setContactFlags(
contact,
teleBase: teleBaseEnabled,
teleLoc: teleLocEnabled,
teleEnv: teleEnvEnabled,
);
Navigator.pop(context);
},

Copilot AI Mar 21, 2026

Copy link

Choose a reason for hiding this comment

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

In the contact settings dialog, connector.setContactFlags(...) returns a Future but it's called without await/error handling. With flutter_lints this may trigger unawaited_futures, and failures will be unhandled.

Make the callback async, await the update, and consider showing an error/snackbar on failure before closing the dialog.

Copilot uses AI. Check for mistakes.
@zjs81 zjs81 merged commit 4c492f6 into main Mar 24, 2026
6 checks passed
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.

3 participants