Add hop width bit display display. Closes #323.

This commit is contained in:
Jack Kingsman
2026-07-10 15:23:46 -07:00
parent f8f6842d1d
commit d2e3b05a38
10 changed files with 293 additions and 32 deletions
+61
View File
@@ -4,6 +4,7 @@ import { useState } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MessageList } from '../components/MessageList';
import { PathHopWidthProvider } from '../contexts/PathHopWidthContext';
import { CONTACT_TYPE_ROOM, type Contact, type Message } from '../types';
const scrollIntoViewMock = vi.fn();
@@ -87,6 +88,66 @@ describe('MessageList channel sender rendering', () => {
expect(screen.queryByText('nl-gr')).not.toBeInTheDocument();
});
it('shows per-hop byte width in the path badge when the toggle is on', () => {
render(
<PathHopWidthProvider showPathHopWidth setShowPathHopWidth={() => {}}>
<MessageList
messages={[
createMessage({
sender_name: 'Alice',
// 8 hex chars over 2 hops = 2 bytes/hop.
paths: [{ path: 'AABBCCDD', path_len: 2, received_at: 1700000001 }],
}),
]}
contacts={[]}
loading={false}
/>
</PathHopWidthProvider>
);
expect(screen.getByText('(2 · 2B)')).toBeInTheDocument();
expect(screen.getByTitle('View message path (2B per hop)')).toBeInTheDocument();
});
it('hides the width by default (toggle off) and shows only the hop count', () => {
render(
<MessageList
messages={[
createMessage({
sender_name: 'Alice',
paths: [{ path: 'AABBCCDD', path_len: 2, received_at: 1700000001 }],
}),
]}
contacts={[]}
loading={false}
/>
);
expect(screen.getByText('(2)')).toBeInTheDocument();
expect(screen.queryByText('(2 · 2B)')).not.toBeInTheDocument();
expect(screen.getByTitle('View message path')).toBeInTheDocument();
});
it('omits the width for direct (0-hop) paths even when the toggle is on', () => {
render(
<PathHopWidthProvider showPathHopWidth setShowPathHopWidth={() => {}}>
<MessageList
messages={[
createMessage({
sender_name: 'Alice',
paths: [{ path: '', path_len: 0, received_at: 1700000001 }],
}),
]}
contacts={[]}
loading={false}
/>
</PathHopWidthProvider>
);
expect(screen.getByText('(d)')).toBeInTheDocument();
expect(screen.getByTitle('View message path')).toBeInTheDocument();
});
it('prefers stored sender_name for channel messages even when text is not sender-prefixed', () => {
render(
<MessageList
+62
View File
@@ -11,6 +11,7 @@ import {
resolvePath,
formatDistance,
formatHopCounts,
formatPathHopWidths,
} from '../utils/pathUtils';
import type { Contact, RadioConfig } from '../types';
import { CONTACT_TYPE_REPEATER } from '../types';
@@ -816,3 +817,64 @@ describe('formatHopCounts', () => {
expect(result.hasMultiple).toBe(true);
});
});
describe('formatPathHopWidths', () => {
it('returns null for null or empty paths', () => {
expect(formatPathHopWidths(null)).toBeNull();
expect(formatPathHopWidths([])).toBeNull();
});
it('returns null for direct (0-hop) paths', () => {
expect(formatPathHopWidths([{ path: '', received_at: 1700000000 }])).toBeNull();
});
it('returns null for legacy paths without hop metadata', () => {
// No path_len -> width is not derivable, so we show nothing rather than guess.
expect(formatPathHopWidths([{ path: 'AABBCCDD', received_at: 1700000000 }])).toBeNull();
});
it('derives 1-byte width', () => {
expect(formatPathHopWidths([{ path: '1A2B', path_len: 2, received_at: 1700000000 }])).toBe(
'1B'
);
});
it('derives 2-byte width', () => {
expect(formatPathHopWidths([{ path: 'AABBCCDD', path_len: 2, received_at: 1700000000 }])).toBe(
'2B'
);
});
it('derives 3-byte width', () => {
expect(
formatPathHopWidths([{ path: 'AABBCCDDEEFF', path_len: 2, received_at: 1700000000 }])
).toBe('3B');
});
it('dedupes identical widths across repeat paths', () => {
expect(
formatPathHopWidths([
{ path: 'AABBCCDD', path_len: 2, received_at: 1700000000 },
{ path: '11223344', path_len: 2, received_at: 1700000001 },
])
).toBe('2B');
});
it('ignores direct paths when other paths have a width', () => {
expect(
formatPathHopWidths([
{ path: '', received_at: 1700000000 },
{ path: 'AABBCCDD', path_len: 2, received_at: 1700000001 },
])
).toBe('2B');
});
it('joins mixed widths sorted ascending', () => {
expect(
formatPathHopWidths([
{ path: 'AABBCCDD', path_len: 2, received_at: 1700000000 }, // 2-byte
{ path: '1A2B', path_len: 2, received_at: 1700000001 }, // 1-byte
])
).toBe('1B/2B');
});
});
+14
View File
@@ -22,6 +22,7 @@ import {
} from '../utils/lastViewedConversation';
import { api } from '../api';
import { DISTANCE_UNIT_KEY } from '../utils/distanceUnits';
import { SHOW_PATH_HOP_WIDTH_KEY } from '../utils/pathHopWidthPreference';
import {
DEFAULT_FONT_SCALE,
FONT_SCALE_KEY,
@@ -656,6 +657,19 @@ describe('SettingsModal', () => {
expect(localStorage.getItem(LAST_VIEWED_CONVERSATION_KEY)).toBeNull();
});
it('defaults the path-hop-width toggle to off and persists enabling it', () => {
renderModal();
openLocalSection();
const checkbox = screen.getByLabelText('Show Path Hop Width');
expect(checkbox).not.toBeChecked();
expect(localStorage.getItem(SHOW_PATH_HOP_WIDTH_KEY)).toBeNull();
fireEvent.click(checkbox);
expect(localStorage.getItem(SHOW_PATH_HOP_WIDTH_KEY)).toBe('true');
});
it('defaults distance units to metric and stores local changes', () => {
renderModal();
openLocalSection();