mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-02 06:53:09 +02:00
Fix unscoped flood-scope handling
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -154,10 +154,10 @@ const publicChannel = {
|
||||
// contacts, unreads) that must all settle before conversation selection
|
||||
// renders. This suite passes in isolation, but under the full parallel suite
|
||||
// (~70 files) CPU contention can stretch that startup well past RTL's 1000ms
|
||||
// default — and even past vitest's 5000ms test timeout — for any waitFor in
|
||||
// this file. It's starvation, not a hang, so give this file generous headroom
|
||||
// on both timeouts: a healthy render still settles in ~100ms (nothing is
|
||||
// slowed), only a starved run waits longer.
|
||||
// default — and occasionally past 10s on GitHub's shared runners — for any
|
||||
// waitFor in this file. It's starvation, not a hang, so give this file generous
|
||||
// headroom on both timeouts: a healthy render still settles in ~100ms (nothing
|
||||
// is slowed), only a starved run waits longer.
|
||||
//
|
||||
// These MUST run at module scope, not in beforeAll: vitest bakes each test's
|
||||
// timeout in when it() registers the test (during collection, before any
|
||||
@@ -167,8 +167,8 @@ const publicChannel = {
|
||||
// 10s but the test killed at 5s) that flaked under load. vi.setConfig is scoped
|
||||
// to this file (each file runs in its own isolated worker), so other files are
|
||||
// unaffected.
|
||||
vi.setConfig({ testTimeout: 15000 });
|
||||
configure({ asyncUtilTimeout: 10000 });
|
||||
vi.setConfig({ testTimeout: 45000 });
|
||||
configure({ asyncUtilTimeout: 30000 });
|
||||
|
||||
describe('App startup hash resolution', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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, '*');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user