From c721e7461a9ae9420ee88d3ecdcf6596fbafe452 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sun, 24 May 2026 13:35:47 -0700 Subject: [PATCH] Don't display blocked contacts on the map. Closes #269. --- frontend/src/App.tsx | 2 + frontend/src/components/ConversationPane.tsx | 6 ++ frontend/src/components/MapView.tsx | 14 ++- frontend/src/test/mapView.test.tsx | 100 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2ae1a42..9d22df3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -680,6 +680,8 @@ export function App() { onToggleTrackedTelemetry: handleToggleTrackedTelemetry, repeaterAutoLoginKey, onClearRepeaterAutoLogin: () => setRepeaterAutoLoginKey(null), + blockedKeys: appSettings?.blocked_keys, + blockedNames: appSettings?.blocked_names, }; const searchProps = { contacts, diff --git a/frontend/src/components/ConversationPane.tsx b/frontend/src/components/ConversationPane.tsx index 6505cc8..942f399 100644 --- a/frontend/src/components/ConversationPane.tsx +++ b/frontend/src/components/ConversationPane.tsx @@ -92,6 +92,8 @@ interface ConversationPaneProps { onToggleTrackedTelemetry: (publicKey: string) => Promise; repeaterAutoLoginKey: string | null; onClearRepeaterAutoLogin: () => void; + blockedKeys?: string[]; + blockedNames?: string[]; } function LoadingPane({ label }: { label: string }) { @@ -171,6 +173,8 @@ export function ConversationPane({ onToggleTrackedTelemetry, repeaterAutoLoginKey, onClearRepeaterAutoLogin, + blockedKeys, + blockedNames, }: ConversationPaneProps) { const [roomAuthenticated, setRoomAuthenticated] = useState(false); const activeContactIsRepeater = useMemo(() => { @@ -215,6 +219,8 @@ export function ConversationPane({ focusedKey={activeConversation.mapFocusKey} rawPackets={rawPackets} config={config} + blockedKeys={blockedKeys} + blockedNames={blockedNames} onSelectContact={(contact) => onSelectConversation({ type: 'contact', diff --git a/frontend/src/components/MapView.tsx b/frontend/src/components/MapView.tsx index 6a1c6d1..fd08b66 100644 --- a/frontend/src/components/MapView.tsx +++ b/frontend/src/components/MapView.tsx @@ -30,6 +30,8 @@ interface MapViewProps { focusedKey?: string | null; rawPackets?: RawPacket[]; config?: RadioConfig | null; + blockedKeys?: string[]; + blockedNames?: string[]; /** When provided, the contact name in each popup becomes a clickable link * that opens the conversation for that contact (DM, repeater, or room). */ onSelectContact?: (contact: Contact) => void; @@ -496,6 +498,8 @@ export function MapView({ focusedKey, rawPackets, config, + blockedKeys, + blockedNames, onSelectContact, }: MapViewProps) { const [sevenDaysAgo] = useState(() => Date.now() / 1000 - 7 * 24 * 60 * 60); @@ -563,10 +567,14 @@ export function MapView({ // Filter contacts for map display const mappableContacts = useMemo(() => { + const isBlocked = (c: Contact) => + (blockedKeys?.length && blockedKeys.includes(c.public_key.toLowerCase())) || + (blockedNames?.length && c.name != null && blockedNames.includes(c.name)); + if (showPackets && discoveryMode) { // Discovery mode: only show nodes that have appeared in resolved packets return contacts.filter( - (c) => isValidLocation(c.lat, c.lon) && discoveredKeys.has(c.public_key) + (c) => isValidLocation(c.lat, c.lon) && discoveredKeys.has(c.public_key) && !isBlocked(c) ); } if (showPackets) { @@ -574,12 +582,14 @@ export function MapView({ return contacts.filter( (c) => isValidLocation(c.lat, c.lon) && + !isBlocked(c) && (c.public_key === focusedKey || (c.last_seen != null && c.last_seen > threeDaysAgoSec)) ); } return contacts.filter( (c) => isValidLocation(c.lat, c.lon) && + !isBlocked(c) && (c.public_key === focusedKey || (c.last_seen != null && c.last_seen > sevenDaysAgo)) ); }, [ @@ -590,6 +600,8 @@ export function MapView({ showPackets, discoveryMode, discoveredKeys, + blockedKeys, + blockedNames, ]); // Resolve a path of hop tokens to geographic waypoints (only unambiguous + has GPS) diff --git a/frontend/src/test/mapView.test.tsx b/frontend/src/test/mapView.test.tsx index db04638..d299796 100644 --- a/frontend/src/test/mapView.test.tsx +++ b/frontend/src/test/mapView.test.tsx @@ -172,4 +172,104 @@ describe('MapView', () => { vi.useRealTimers(); } }); + + it('excludes contacts whose public key is in blockedKeys', () => { + const visible: Contact = { + public_key: 'aa'.repeat(32), + name: 'Visible', + type: 1, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 40, + lon: -74, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + const blocked: Contact = { + public_key: 'bb'.repeat(32), + name: 'Blocked', + type: 2, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 41, + lon: -73, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + + render(); + + expect(screen.getByText('Visible')).toBeInTheDocument(); + expect(screen.queryByText('Blocked')).toBeNull(); + }); + + it('excludes contacts whose name is in blockedNames', () => { + const visible: Contact = { + public_key: 'aa'.repeat(32), + name: 'Visible', + type: 1, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 40, + lon: -74, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + const blocked: Contact = { + public_key: 'cc'.repeat(32), + name: 'BadActor', + type: 2, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: -1, + route_override_path: null, + route_override_len: null, + route_override_hash_mode: null, + last_advert: null, + lat: 41, + lon: -73, + last_seen: Math.floor(Date.now() / 1000), + on_radio: false, + favorite: false, + last_contacted: null, + last_read_at: null, + first_seen: null, + }; + + render(); + + expect(screen.getByText('Visible')).toBeInTheDocument(); + expect(screen.queryByText('BadActor')).toBeNull(); + }); });