Path display improvements, focusable maps, contact distance display, click to copy keys

This commit is contained in:
Jack Kingsman
2026-01-18 16:08:39 -08:00
parent 05a830d63f
commit cc1a2c57c2
14 changed files with 971 additions and 573 deletions
+29 -1
View File
@@ -10,8 +10,16 @@ export interface PathHop {
export interface ResolvedPath {
sender: { name: string; prefix: string; lat: number | null; lon: number | null };
hops: PathHop[];
receiver: { name: string; prefix: string; lat: number | null; lon: number | null };
receiver: {
name: string;
prefix: string;
lat: number | null;
lon: number | null;
publicKey: string | null;
};
totalDistances: number[] | null; // Single-element array with sum of unambiguous distances
/** True if path has any gaps (unknown, ambiguous, or missing location hops) */
hasGaps: boolean;
}
export interface SenderInfo {
@@ -101,6 +109,16 @@ export function isValidLocation(lat: number | null, lon: number | null): boolean
return true;
}
/**
* Format distance in human-readable form (m or km)
*/
export function formatDistance(km: number): string {
if (km < 1) {
return `${Math.round(km * 1000)}m`;
}
return `${km.toFixed(1)}km`;
}
/**
* Sort contacts by distance from a reference point
* Contacts without location are placed at the end
@@ -164,6 +182,7 @@ export function resolvePath(
prefix: receiverPrefix,
lat: config?.lat ?? null,
lon: config?.lon ?? null,
publicKey: config?.public_key ?? null,
};
// Build hops
@@ -229,11 +248,20 @@ export function resolvePath(
// Calculate total distances (can be multiple if ambiguous)
const totalDistances = calculateTotalDistances(resolvedSender, hops, resolvedReceiver);
// Determine if path has any gaps (unknown, ambiguous, or missing location)
const hasGaps =
!isValidLocation(resolvedSender.lat, resolvedSender.lon) ||
!isValidLocation(resolvedReceiver.lat, resolvedReceiver.lon) ||
hops.some(
(hop) => hop.matches.length !== 1 || !isValidLocation(hop.matches[0].lat, hop.matches[0].lon)
);
return {
sender: resolvedSender,
hops,
receiver: resolvedReceiver,
totalDistances,
hasGaps,
};
}
+20 -1
View File
@@ -3,9 +3,11 @@ import type { Conversation } from '../types';
export interface ParsedHashConversation {
type: 'channel' | 'contact' | 'raw' | 'map';
name: string;
/** For map view: public key prefix to focus on */
mapFocusKey?: string;
}
// Parse URL hash to get conversation (e.g., #channel/Public or #contact/JohnDoe or #raw)
// Parse URL hash to get conversation (e.g., #channel/Public or #contact/JohnDoe or #raw or #map/focus/ABCD1234)
export function parseHashConversation(): ParsedHashConversation | null {
const hash = window.location.hash.slice(1); // Remove leading #
if (!hash) return null;
@@ -18,6 +20,15 @@ export function parseHashConversation(): ParsedHashConversation | null {
return { type: 'map', name: 'map' };
}
// Check for map with focus: #map/focus/{pubkey_prefix}
if (hash.startsWith('map/focus/')) {
const focusKey = hash.slice('map/focus/'.length);
if (focusKey) {
return { type: 'map', name: 'map', mapFocusKey: decodeURIComponent(focusKey) };
}
return { type: 'map', name: 'map' };
}
const slashIndex = hash.indexOf('/');
if (slashIndex === -1) return null;
@@ -30,6 +41,14 @@ export function parseHashConversation(): ParsedHashConversation | null {
return null;
}
/**
* Generate a URL hash for focusing on a contact in the map view
* @param publicKeyPrefix - The public key or prefix to focus on
*/
export function getMapFocusHash(publicKeyPrefix: string): string {
return `#map/focus/${encodeURIComponent(publicKeyPrefix)}`;
}
// Generate URL hash from conversation
export function getConversationHash(conv: Conversation | null): string {
if (!conv) return '';