Add channel path hash width override

This commit is contained in:
Jack Kingsman
2026-04-03 13:05:58 -07:00
parent d802dd4212
commit 8e998c03ba
16 changed files with 374 additions and 22 deletions
@@ -0,0 +1,132 @@
import { useEffect, useState } from 'react';
import { Button } from './ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Label } from './ui/label';
const PATH_HASH_MODE_LABELS: Record<number, string> = {
0: '1-byte',
1: '2-byte',
2: '3-byte',
};
interface ChannelPathHashModeOverrideModalProps {
open: boolean;
onClose: () => void;
channelName: string;
currentOverride: number | null;
radioDefault: number;
onSetOverride: (value: number | null) => void;
}
export function ChannelPathHashModeOverrideModal({
open,
onClose,
channelName,
currentOverride,
radioDefault,
onSetOverride,
}: ChannelPathHashModeOverrideModalProps) {
const [selected, setSelected] = useState<number | null>(null);
useEffect(() => {
if (open) {
setSelected(currentOverride);
}
}, [currentOverride, open]);
const radioDefaultLabel = PATH_HASH_MODE_LABELS[radioDefault] ?? `${radioDefault}`;
const options: { value: number | null; label: string; description: string }[] = [
{
value: null,
label: `Radio default (${radioDefaultLabel})`,
description: 'Use the radio-wide path hash mode setting',
},
{
value: 0,
label: '1-byte hop identifiers',
description: 'Shortest paths, least repeater disambiguation',
},
{
value: 1,
label: '2-byte hop identifiers',
description: 'Better repeater disambiguation',
},
{
value: 2,
label: '3-byte hop identifiers',
description: 'Best repeater disambiguation, longest paths',
},
];
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle>Path Hop Width Override</DialogTitle>
<DialogDescription>
Override the path hash mode for this channel. Wider hop identifiers improve repeater
disambiguation but extend send time and will prevent users on old (&lt;1.14) firmware
from receiving the message.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-md border border-border bg-muted/20 p-3 text-sm">
<div className="font-medium">{channelName}</div>
<div className="mt-1 text-muted-foreground">
Current override:{' '}
{currentOverride != null
? (PATH_HASH_MODE_LABELS[currentOverride] ?? `mode ${currentOverride}`)
: `none (using radio default: ${radioDefaultLabel})`}
</div>
</div>
<div className="space-y-2">
<Label>Hop width for this channel</Label>
<div className="space-y-1.5">
{options.map((opt) => (
<button
key={String(opt.value)}
type="button"
className={`w-full rounded-md border px-3 py-2 text-left text-sm transition-colors ${
selected === opt.value
? 'border-primary bg-primary/10 text-foreground'
: 'border-border hover:bg-accent'
}`}
onClick={() => setSelected(opt.value)}
>
<div className="font-medium">{opt.label}</div>
<div className="text-xs text-muted-foreground">{opt.description}</div>
</button>
))}
</div>
</div>
</div>
<DialogFooter className="gap-2 sm:block sm:space-x-0">
<Button
type="button"
className="w-full"
onClick={() => {
onSetOverride(selected);
onClose();
}}
>
{selected == null
? `Use radio default for ${channelName}`
: `Use ${PATH_HASH_MODE_LABELS[selected]} hops for ${channelName}`}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+40 -1
View File
@@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import { Bell, Globe2, Info, Route, Star, Trash2 } from 'lucide-react';
import { Bell, ChevronsLeftRight, Globe2, Info, Route, Star, Trash2 } from 'lucide-react';
import { toast } from './ui/sonner';
import { DirectTraceIcon } from './DirectTraceIcon';
import { ContactPathDiscoveryModal } from './ContactPathDiscoveryModal';
import { ChannelFloodScopeOverrideModal } from './ChannelFloodScopeOverrideModal';
import { ChannelPathHashModeOverrideModal } from './ChannelPathHashModeOverrideModal';
import { isFavorite } from '../utils/favorites';
import { handleKeyboardActivate } from '../utils/a11y';
import { isPublicChannelKey } from '../utils/publicChannel';
@@ -36,6 +37,7 @@ interface ChatHeaderProps {
onToggleNotifications: () => void;
onToggleFavorite: (type: 'channel' | 'contact', id: string) => void;
onSetChannelFloodScopeOverride?: (key: string, floodScopeOverride: string) => void;
onSetChannelPathHashModeOverride?: (key: string, pathHashModeOverride: number | null) => void;
onDeleteChannel: (key: string) => void;
onDeleteContact: (publicKey: string) => void;
onOpenContactInfo?: (publicKey: string) => void;
@@ -56,6 +58,7 @@ export function ChatHeader({
onToggleNotifications,
onToggleFavorite,
onSetChannelFloodScopeOverride,
onSetChannelPathHashModeOverride,
onDeleteChannel,
onDeleteContact,
onOpenContactInfo,
@@ -64,11 +67,13 @@ export function ChatHeader({
const [showKey, setShowKey] = useState(false);
const [pathDiscoveryOpen, setPathDiscoveryOpen] = useState(false);
const [channelOverrideOpen, setChannelOverrideOpen] = useState(false);
const [pathHashModeOverrideOpen, setPathHashModeOverrideOpen] = useState(false);
useEffect(() => {
setShowKey(false);
setPathDiscoveryOpen(false);
setChannelOverrideOpen(false);
setPathHashModeOverrideOpen(false);
}, [conversation.id]);
const activeChannel =
@@ -81,6 +86,12 @@ export function ChatHeader({
? stripRegionScopePrefix(activeFloodScopeOverride)
: null;
const activeFloodScopeDisplay = activeFloodScopeOverride ? activeFloodScopeOverride : null;
const activePathHashModeOverride =
conversation.type === 'channel' ? (activeChannel?.path_hash_mode_override ?? null) : null;
const showPathHashModeOverride =
conversation.type === 'channel' &&
onSetChannelPathHashModeOverride &&
config?.path_hash_mode_supported;
const isPrivateChannel = conversation.type === 'channel' && !activeChannel?.is_hashtag;
const activeContact =
conversation.type === 'contact'
@@ -108,6 +119,11 @@ export function ChatHeader({
setChannelOverrideOpen(true);
};
const handleEditPathHashModeOverride = () => {
if (conversation.type !== 'channel' || !onSetChannelPathHashModeOverride) return;
setPathHashModeOverrideOpen(true);
};
const handleOpenConversationInfo = () => {
if (conversation.type === 'contact' && onOpenContactInfo) {
onOpenContactInfo(conversation.id);
@@ -323,6 +339,19 @@ export function ChatHeader({
)}
</button>
)}
{showPathHashModeOverride && (
<button
className="flex shrink-0 items-center gap-1 rounded px-1 py-1 text-lg leading-none transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={handleEditPathHashModeOverride}
title="Set path hop width override"
aria-label="Set path hop width override"
>
<ChevronsLeftRight
className={`h-4 w-4 ${activePathHashModeOverride != null ? 'text-status-connected' : 'text-muted-foreground'}`}
aria-hidden="true"
/>
</button>
)}
{(conversation.type === 'channel' || conversation.type === 'contact') && (
<button
className="p-1 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
@@ -379,6 +408,16 @@ export function ChatHeader({
onSetOverride={(value) => onSetChannelFloodScopeOverride(conversation.id, value)}
/>
)}
{showPathHashModeOverride && (
<ChannelPathHashModeOverrideModal
open={pathHashModeOverrideOpen}
onClose={() => setPathHashModeOverrideOpen(false)}
channelName={conversation.name}
currentOverride={activePathHashModeOverride}
radioDefault={config?.path_hash_mode ?? 0}
onSetOverride={(value) => onSetChannelPathHashModeOverride(conversation.id, value)}
/>
)}
</header>
);
}
@@ -63,6 +63,10 @@ interface ConversationPaneProps {
onDeleteContact: (publicKey: string) => Promise<void>;
onDeleteChannel: (key: string) => Promise<void>;
onSetChannelFloodScopeOverride: (channelKey: string, floodScopeOverride: string) => Promise<void>;
onSetChannelPathHashModeOverride?: (
channelKey: string,
pathHashModeOverride: number | null
) => Promise<void>;
onOpenContactInfo: (publicKey: string, fromChannel?: boolean) => void;
onOpenChannelInfo: (channelKey: string) => void;
onSenderClick: (sender: string) => void;
@@ -131,6 +135,7 @@ export function ConversationPane({
onDeleteContact,
onDeleteChannel,
onSetChannelFloodScopeOverride,
onSetChannelPathHashModeOverride,
onOpenContactInfo,
onOpenChannelInfo,
onSenderClick,
@@ -259,6 +264,7 @@ export function ConversationPane({
onToggleNotifications={onToggleNotifications}
onToggleFavorite={onToggleFavorite}
onSetChannelFloodScopeOverride={onSetChannelFloodScopeOverride}
onSetChannelPathHashModeOverride={onSetChannelPathHashModeOverride}
onDeleteChannel={onDeleteChannel}
onDeleteContact={onDeleteContact}
onOpenContactInfo={onOpenContactInfo}