Gate scoping-safety on firmware ref, and and make per-channel override tristate.

This commit is contained in:
Jack Kingsman
2026-07-08 15:45:58 -07:00
parent 30e9a77cdc
commit 6edd2927b1
18 changed files with 313 additions and 49 deletions
@@ -1,6 +1,10 @@
import { useEffect, useState } from 'react';
import { stripRegionScopePrefix } from '../utils/regionScope';
import {
UNSCOPED_OVERRIDE_MARKER,
isUnscopedMarker,
stripRegionScopePrefix,
} from '../utils/regionScope';
import { Button } from './ui/button';
import {
Dialog,
@@ -34,11 +38,18 @@ export function ChannelFloodScopeOverrideModal({
if (!open) {
return;
}
setRegion(stripRegionScopePrefix(currentOverride));
// The unscoped marker isn't a region name, so start the input blank for it.
setRegion(isUnscopedMarker(currentOverride) ? '' : stripRegionScopePrefix(currentOverride));
}, [currentOverride, open]);
const trimmedRegion = region.trim();
const currentOverrideLabel = isUnscopedMarker(currentOverride)
? 'unscoped (plain flood)'
: currentOverride
? stripRegionScopePrefix(currentOverride)
: 'inherit global setting';
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[520px]">
@@ -46,7 +57,9 @@ export function ChannelFloodScopeOverrideModal({
<DialogTitle>Regional Override</DialogTitle>
<DialogDescription>
Channel-level regional routing temporarily changes the radio flood scope before send and
restores it after. This can noticeably slow channel sends.
restores it after. This can noticeably slow channel sends. Choose one of three modes
below: scope to a region, force unscoped (plain flood, ignoring your global region), or
inherit the global setting.
</DialogDescription>
</DialogHeader>
@@ -54,8 +67,7 @@ export function ChannelFloodScopeOverrideModal({
<div className="rounded-md border border-border bg-muted/20 p-3 text-sm">
<div className="font-medium">{roomName}</div>
<div className="mt-1 text-muted-foreground">
Current regional override:{' '}
{currentOverride ? stripRegionScopePrefix(currentOverride) : 'none'}
Current setting: {currentOverrideLabel}
</div>
</div>
@@ -83,8 +95,19 @@ export function ChannelFloodScopeOverrideModal({
}}
>
{trimmedRegion.length > 0
? `Use ${trimmedRegion} region for ${roomName}`
: `Use region for ${roomName}`}
? `Scope ${roomName} to ${trimmedRegion}`
: `Scope ${roomName} to a region`}
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => {
onSetOverride(UNSCOPED_OVERRIDE_MARKER);
onClose();
}}
>
Always send {roomName} unscoped (ignore global region)
</Button>
<Button
type="button"
@@ -95,7 +118,7 @@ export function ChannelFloodScopeOverrideModal({
onClose();
}}
>
Do not use region routing for {roomName}
Use global region setting for {roomName}
</Button>
</div>
</DialogFooter>
@@ -1163,7 +1163,9 @@ export function SettingsRadioSection({
<p className="text-[0.8125rem] text-muted-foreground">
Tag outgoing messages with a region name (e.g. MyRegion). Repeaters configured for that
region can forward the traffic, while repeaters configured to deny other regions may drop
it. Leave empty to disable.
it. Leave empty to send unscoped (plain flood). Forcing unscoped when a default scope is
configured on the radio requires firmware v12 or newer. Individual channels can override
this with their own region or an "always unscoped" setting.
</p>
</div>
@@ -360,8 +360,34 @@ describe('ChatHeader key visibility', () => {
expect(await screen.findByRole('dialog')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Region'), { target: { value: 'Esperance' } });
fireEvent.click(screen.getByRole('button', { name: 'Use Esperance region for #flightless' }));
fireEvent.click(screen.getByRole('button', { name: 'Scope #flightless to Esperance' }));
expect(onSetChannelFloodScopeOverride).toHaveBeenCalledWith(key, 'Esperance');
});
it('forces the channel unscoped via the modal (issue #303)', async () => {
const key = 'CD'.repeat(16);
const channel = makeChannel(key, '#flightless', true);
const conversation: Conversation = { type: 'channel', id: key, name: '#flightless' };
const onSetChannelFloodScopeOverride = vi.fn();
render(
<ChatHeader
{...baseProps}
conversation={conversation}
channels={[channel]}
onSetChannelFloodScopeOverride={onSetChannelFloodScopeOverride}
/>
);
fireEvent.click(screen.getByTitle('Set regional override'));
expect(await screen.findByRole('dialog')).toBeInTheDocument();
fireEvent.click(
screen.getByRole('button', {
name: 'Always send #flightless unscoped (ignore global region)',
})
);
expect(onSetChannelFloodScopeOverride).toHaveBeenCalledWith(key, '*');
});
});
+9
View File
@@ -1,3 +1,12 @@
// Canonical persisted marker meaning "force this channel unscoped / plain flood"
// (mirrors the backend UNSCOPED_OVERRIDE_MARKER). Distinct from null, which means
// "inherit the global scope".
export const UNSCOPED_OVERRIDE_MARKER = '*';
export function isUnscopedMarker(scope: string | null | undefined): boolean {
return scope === UNSCOPED_OVERRIDE_MARKER;
}
export function stripRegionScopePrefix(scope: string | null | undefined): string {
if (!scope) return '';
return scope.startsWith('#') ? scope.slice(1) : scope;