diff --git a/frontend/src/components/MessageList.tsx b/frontend/src/components/MessageList.tsx index 7602b01..36dee9f 100644 --- a/frontend/src/components/MessageList.tsx +++ b/frontend/src/components/MessageList.tsx @@ -380,8 +380,9 @@ export function MessageList({ publicKeyOrPrefix: config?.public_key || '', lat: config?.lat ?? null, lon: config?.lon ?? null, + pathHashMode: config?.path_hash_mode ?? null, }), - [config?.name, config?.public_key, config?.lat, config?.lon] + [config?.name, config?.public_key, config?.lat, config?.lon, config?.path_hash_mode] ); // Derive live so the byte-perfect button disables if the 30s window expires while modal is open @@ -411,6 +412,7 @@ export function MessageList({ publicKeyOrPrefix: contact.public_key, lat: contact.lat, lon: contact.lon, + pathHashMode: contact.out_path_hash_mode, }; } // For channel messages, try to find contact by parsed sender name @@ -422,6 +424,7 @@ export function MessageList({ publicKeyOrPrefix: senderContact.public_key, lat: senderContact.lat, lon: senderContact.lon, + pathHashMode: senderContact.out_path_hash_mode, }; } } @@ -431,6 +434,7 @@ export function MessageList({ publicKeyOrPrefix: msg.conversation_key || '', lat: null, lon: null, + pathHashMode: null, }; }; diff --git a/frontend/src/test/pathUtils.test.ts b/frontend/src/test/pathUtils.test.ts index 1003978..e19fe20 100644 --- a/frontend/src/test/pathUtils.test.ts +++ b/frontend/src/test/pathUtils.test.ts @@ -324,6 +324,33 @@ describe('resolvePath', () => { expect(result.receiver.name).toBe('MyRadio'); }); + it('uses explicit sender and receiver multibyte modes for endpoint prefixes', () => { + const result = resolvePath( + '', + { ...sender, pathHashMode: 1 }, + contacts, + createConfig({ + public_key: 'ABCDEF' + 'F'.repeat(58), + path_hash_mode: 2, + }) + ); + + expect(result.sender.prefix).toBe('5EEE'); + expect(result.receiver.prefix).toBe('ABCDEF'); + }); + + it('derives sender multibyte width from path metadata when sender mode is unknown', () => { + const result = resolvePath( + '1A2B3C4D', + { ...sender, publicKeyOrPrefix: 'AABBCCDDEEFF' + '0'.repeat(52), pathHashMode: null }, + contacts, + config, + 2 + ); + + expect(result.sender.prefix).toBe('AABB'); + }); + it('handles null config gracefully', () => { const result = resolvePath('1A', sender, contacts, null); diff --git a/frontend/src/utils/pathUtils.ts b/frontend/src/utils/pathUtils.ts index baf49c2..8bfb827 100644 --- a/frontend/src/utils/pathUtils.ts +++ b/frontend/src/utils/pathUtils.ts @@ -29,6 +29,46 @@ export interface SenderInfo { publicKeyOrPrefix: string; lat: number | null; lon: number | null; + pathHashMode?: number | null; +} + +function normalizePathHashMode(mode: number | null | undefined): number | null { + if (mode == null || !Number.isInteger(mode) || mode < 0 || mode > 2) { + return null; + } + return mode; +} + +function inferPathHashMode( + path: string | null | undefined, + hopCount?: number | null +): number | null { + if (!path || path.length === 0 || hopCount == null || hopCount <= 0) { + return null; + } + + const charsPerHop = path.length / hopCount; + if ( + charsPerHop < 2 || + charsPerHop > 6 || + charsPerHop % 2 !== 0 || + charsPerHop * hopCount !== path.length + ) { + return null; + } + + return charsPerHop / 2 - 1; +} + +function formatEndpointPrefix(key: string | null | undefined, pathHashMode: number | null): string { + if (!key) { + return '??'; + } + + const normalized = key.toUpperCase(); + const hashMode = normalizePathHashMode(pathHashMode) ?? 0; + const chars = (hashMode + 1) * 2; + return normalized.slice(0, Math.min(chars, normalized.length)); } /** @@ -269,9 +309,13 @@ export function resolvePath( hopCount?: number | null ): ResolvedPath { const hopPrefixes = parsePathHops(path, hopCount); + const inferredPathHashMode = inferPathHashMode(path, hopCount); // Build sender info - const senderPrefix = sender.publicKeyOrPrefix.toUpperCase().slice(0, 2); + const senderPrefix = formatEndpointPrefix( + sender.publicKeyOrPrefix, + normalizePathHashMode(sender.pathHashMode) ?? inferredPathHashMode + ); const resolvedSender = { name: sender.name, prefix: senderPrefix, @@ -280,7 +324,10 @@ export function resolvePath( }; // Build receiver info from radio config - const receiverPrefix = config?.public_key?.toUpperCase().slice(0, 2) || '??'; + const receiverPrefix = formatEndpointPrefix( + config?.public_key, + normalizePathHashMode(config?.path_hash_mode) ?? inferredPathHashMode + ); const resolvedReceiver = { name: config?.name || 'Unknown', prefix: receiverPrefix,