From 41a297c944617b9694dd5453444872321e07b21a Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 18 Mar 2026 22:38:43 -0700 Subject: [PATCH] GIVE ME SMOOTS. Closes #87. --- frontend/src/App.tsx | 53 ++++++++++--------- frontend/src/components/ContactInfoPane.tsx | 4 +- frontend/src/components/ContactStatusInfo.tsx | 4 +- frontend/src/components/PathModal.tsx | 33 +++++++++--- .../repeater/RepeaterNeighborsPane.tsx | 6 ++- .../settings/SettingsLocalSection.tsx | 32 +++++++++++ frontend/src/contexts/DistanceUnitContext.tsx | 31 +++++++++++ frontend/src/hooks/useAppShell.ts | 6 +++ frontend/src/test/distanceUnits.test.ts | 32 +++++++++++ frontend/src/test/pathUtils.test.ts | 41 +++++++++----- frontend/src/test/settingsModal.test.tsx | 13 +++++ frontend/src/utils/distanceUnits.ts | 32 +++++++++++ frontend/src/utils/pathUtils.ts | 33 ++++++++++-- 13 files changed, 267 insertions(+), 53 deletions(-) create mode 100644 frontend/src/contexts/DistanceUnitContext.tsx create mode 100644 frontend/src/test/distanceUnits.test.ts create mode 100644 frontend/src/utils/distanceUnits.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 197a169..2b43110 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ import { } from './hooks'; import { AppShell } from './components/AppShell'; import type { MessageInputHandle } from './components/MessageInput'; +import { DistanceUnitProvider } from './contexts/DistanceUnitContext'; import { messageContainsMention } from './utils/messageParser'; import { getStateKey } from './utils/conversationState'; import type { Conversation, Message, RawPacket } from './types'; @@ -91,10 +92,12 @@ export function App() { showCracker, crackerRunning, localLabel, + distanceUnit, setSettingsSection, setSidebarOpen, setCrackerRunning, setLocalLabel, + setDistanceUnit, handleCloseSettingsView, handleToggleSettingsView, handleOpenNewMessage, @@ -564,29 +567,31 @@ export function App() { setContactsLoaded, ]); return ( - + + + ); } diff --git a/frontend/src/components/ContactInfoPane.tsx b/frontend/src/components/ContactInfoPane.tsx index db327f5..f9abe0b 100644 --- a/frontend/src/components/ContactInfoPane.tsx +++ b/frontend/src/components/ContactInfoPane.tsx @@ -24,6 +24,7 @@ import { handleKeyboardActivate } from '../utils/a11y'; import { ContactAvatar } from './ContactAvatar'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './ui/sheet'; import { toast } from './ui/sonner'; +import { useDistanceUnit } from '../contexts/DistanceUnitContext'; import type { Contact, ContactActiveRoom, @@ -82,6 +83,7 @@ export function ContactInfoPane({ onToggleBlockedKey, onToggleBlockedName, }: ContactInfoPaneProps) { + const { distanceUnit } = useDistanceUnit(); const isNameOnly = contactKey?.startsWith('name:') ?? false; const nameOnlyValue = isNameOnly && contactKey ? contactKey.slice(5) : null; @@ -315,7 +317,7 @@ export function ContactInfoPane({ )} {distFromUs !== null && ( - + )} {effectiveRoute && ( {contact.lat!.toFixed(3)}, {contact.lon!.toFixed(3)} - {distFromUs !== null && ` (${formatDistance(distFromUs)})`} + {distFromUs !== null && ` (${formatDistance(distFromUs, distanceUnit)})`} ); } diff --git a/frontend/src/components/PathModal.tsx b/frontend/src/components/PathModal.tsx index 727e734..e2e5f68 100644 --- a/frontend/src/components/PathModal.tsx +++ b/frontend/src/components/PathModal.tsx @@ -14,6 +14,8 @@ import { } from '../utils/pathUtils'; import { formatTime } from '../utils/messageParser'; import { getMapFocusHash } from '../utils/urlHash'; +import { useDistanceUnit } from '../contexts/DistanceUnitContext'; +import type { DistanceUnit } from '../utils/distanceUnits'; const PathRouteMap = lazy(() => import('./PathRouteMap').then((m) => ({ default: m.PathRouteMap })) @@ -44,6 +46,7 @@ export function PathModal({ isResendable, onResend, }: PathModalProps) { + const { distanceUnit } = useDistanceUnit(); const [expandedMaps, setExpandedMaps] = useState>(new Set()); const hasResendActions = isOutgoingChan && messageId !== undefined && onResend; const hasPaths = paths.length > 0; @@ -120,7 +123,8 @@ export function PathModal({ resolvedPaths[0].resolved.sender.lon, resolvedPaths[0].resolved.receiver.lat, resolvedPaths[0].resolved.receiver.lon - )! + )!, + distanceUnit )} @@ -171,7 +175,11 @@ export function PathModal({ )} - + ); })} @@ -227,9 +235,10 @@ export function PathModal({ interface PathVisualizationProps { resolved: ResolvedPath; senderInfo: SenderInfo; + distanceUnit: DistanceUnit; } -function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { +function PathVisualization({ resolved, senderInfo, distanceUnit }: PathVisualizationProps) { // Track previous location for each hop to calculate distances // Returns null if previous hop was ambiguous or has invalid location const getPrevLocation = (hopIndex: number): { lat: number | null; lon: number | null } | null => { @@ -264,6 +273,7 @@ function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { name={resolved.sender.name} prefix={resolved.sender.prefix} distance={null} + distanceUnit={distanceUnit} isFirst lat={resolved.sender.lat} lon={resolved.sender.lon} @@ -277,6 +287,7 @@ function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { hop={hop} hopNumber={index + 1} prevLocation={getPrevLocation(index)} + distanceUnit={distanceUnit} /> ))} @@ -286,6 +297,7 @@ function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { name={resolved.receiver.name} prefix={resolved.receiver.prefix} distance={calculateReceiverDistance(resolved)} + distanceUnit={distanceUnit} isLast lat={resolved.receiver.lat} lon={resolved.receiver.lon} @@ -300,7 +312,7 @@ function PathVisualization({ resolved, senderInfo }: PathVisualizationProps) { {resolved.hasGaps ? '>' : ''} - {formatDistance(resolved.totalDistances[0])} + {formatDistance(resolved.totalDistances[0], distanceUnit)} )} @@ -313,6 +325,7 @@ interface PathNodeProps { name: string; prefix: string; distance: number | null; + distanceUnit: DistanceUnit; isFirst?: boolean; isLast?: boolean; /** Optional coordinates for map link */ @@ -327,6 +340,7 @@ function PathNode({ name, prefix, distance, + distanceUnit, isFirst, isLast, lat, @@ -353,7 +367,9 @@ function PathNode({
{name} {distance !== null && ( - - {formatDistance(distance)} + + - {formatDistance(distance, distanceUnit)} + )} {hasLocation && }
@@ -366,9 +382,10 @@ interface HopNodeProps { hop: PathHop; hopNumber: number; prevLocation: { lat: number | null; lon: number | null } | null; + distanceUnit: DistanceUnit; } -function HopNode({ hop, hopNumber, prevLocation }: HopNodeProps) { +function HopNode({ hop, hopNumber, prevLocation, distanceUnit }: HopNodeProps) { const isAmbiguous = hop.matches.length > 1; const isUnknown = hop.matches.length === 0; @@ -417,7 +434,7 @@ function HopNode({ hop, hopNumber, prevLocation }: HopNodeProps) { {contact.name || contact.public_key.slice(0, 12)} {dist !== null && ( - - {formatDistance(dist)} + - {formatDistance(dist, distanceUnit)} )} {hasLocation && ( @@ -436,7 +453,7 @@ function HopNode({ hop, hopNumber, prevLocation }: HopNodeProps) { {hop.matches[0].name || hop.matches[0].public_key.slice(0, 12)} {hop.distanceFromPrev !== null && ( - - {formatDistance(hop.distanceFromPrev)} + - {formatDistance(hop.distanceFromPrev, distanceUnit)} )} {isValidLocation(hop.matches[0].lat, hop.matches[0].lon) && ( diff --git a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx index 0c2d518..d6ae6ba 100644 --- a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx +++ b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx @@ -2,6 +2,7 @@ import { useMemo, lazy, Suspense } from 'react'; import { cn } from '@/lib/utils'; import { RepeaterPane, NotFetched, formatDuration } from './repeaterPaneShared'; import { isValidLocation, calculateDistance, formatDistance } from '../../utils/pathUtils'; +import { useDistanceUnit } from '../../contexts/DistanceUnitContext'; import type { Contact, RepeaterNeighborsResponse, @@ -35,6 +36,7 @@ export function NeighborsPane({ nodeInfoState: PaneState; repeaterName: string | null; }) { + const { distanceUnit } = useDistanceUnit(); const advertLat = repeaterContact?.lat ?? null; const advertLon = repeaterContact?.lon ?? null; @@ -93,7 +95,7 @@ export function NeighborsPane({ if (hasValidRepeaterGps && isValidLocation(nLat, nLon)) { const distKm = calculateDistance(positionSource.lat, positionSource.lon, nLat, nLon); if (distKm != null) { - dist = formatDistance(distKm); + dist = formatDistance(distKm, distanceUnit); anyDist = true; } } @@ -111,7 +113,7 @@ export function NeighborsPane({ sorted: enriched, hasDistances: anyDist, }; - }, [contacts, data, hasValidRepeaterGps, positionSource.lat, positionSource.lon]); + }, [contacts, data, distanceUnit, hasValidRepeaterGps, positionSource.lat, positionSource.lon]); return ( void; className?: string; }) { + const { distanceUnit, setDistanceUnit } = useDistanceUnit(); const [reopenLastConversation, setReopenLastConversation] = useState( getReopenLastConversationEnabled ); @@ -82,6 +89,31 @@ export function SettingsLocalSection({ +
+ + +

+ Controls how distances are shown throughout the app. +

+
+ + +