diff --git a/app/radio.py b/app/radio.py index 9dc9aa6..ed86958 100644 --- a/app/radio.py +++ b/app/radio.py @@ -262,10 +262,11 @@ class RadioManager: # Apply flood scope from settings (best-effort; older firmware # may not support set_flood_scope) + from app.region_scope import normalize_region_scope from app.repository import AppSettingsRepository app_settings = await AppSettingsRepository.get() - scope = app_settings.flood_scope + scope = normalize_region_scope(app_settings.flood_scope) try: await mc.commands.set_flood_scope(scope if scope else "") logger.info("Applied flood_scope=%r", scope or "(disabled)") diff --git a/app/region_scope.py b/app/region_scope.py new file mode 100644 index 0000000..681d4cf --- /dev/null +++ b/app/region_scope.py @@ -0,0 +1,20 @@ +"""Helpers for normalizing MeshCore flood-scope / region names.""" + + +def normalize_region_scope(scope: str | None) -> str: + """Normalize a user-facing region scope into MeshCore's internal form. + + Region names are now user-facing plain strings like ``Esperance``. + Internally, MeshCore still expects hashtag-style names like ``#Esperance``. + + Backward compatibility: + - blank/None stays disabled (`""`) + - existing leading ``#`` is preserved + """ + + stripped = (scope or "").strip() + if not stripped: + return "" + if stripped.startswith("#"): + return stripped + return f"#{stripped}" diff --git a/app/routers/channels.py b/app/routers/channels.py index 8f3831a..f9ea589 100644 --- a/app/routers/channels.py +++ b/app/routers/channels.py @@ -9,6 +9,7 @@ from app.dependencies import require_connected from app.models import Channel, ChannelDetail, ChannelMessageCounts, ChannelTopSender from app.radio import radio_manager from app.radio_sync import upsert_channel_from_radio_slot +from app.region_scope import normalize_region_scope from app.repository import ChannelRepository, MessageRepository from app.websocket import broadcast_event @@ -153,7 +154,7 @@ async def set_channel_flood_scope_override( if not channel: raise HTTPException(status_code=404, detail="Channel not found") - override = request.flood_scope_override.strip() or None + override = normalize_region_scope(request.flood_scope_override) or None updated = await ChannelRepository.update_flood_scope_override(channel.key, override) if not updated: raise HTTPException(status_code=500, detail="Failed to update flood-scope override") diff --git a/app/routers/messages.py b/app/routers/messages.py index 26f8ee2..b15c960 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -14,6 +14,7 @@ from app.models import ( SendDirectMessageRequest, ) from app.radio import radio_manager +from app.region_scope import normalize_region_scope from app.repository import AmbiguousPublicKeyPrefixError, AppSettingsRepository, MessageRepository from app.websocket import broadcast_error, broadcast_event @@ -31,12 +32,12 @@ async def _send_channel_message_with_effective_scope( action_label: str, ) -> Any: """Send a channel message, temporarily overriding flood scope when configured.""" - override_scope = (channel.flood_scope_override or "").strip() + override_scope = normalize_region_scope(channel.flood_scope_override) baseline_scope = "" if override_scope: settings = await AppSettingsRepository.get() - baseline_scope = settings.flood_scope + baseline_scope = normalize_region_scope(settings.flood_scope) if override_scope and override_scope != baseline_scope: logger.info( diff --git a/app/routers/settings.py b/app/routers/settings.py index 7f220a3..a5e053c 100644 --- a/app/routers/settings.py +++ b/app/routers/settings.py @@ -6,6 +6,7 @@ from fastapi import APIRouter from pydantic import BaseModel, Field from app.models import AppSettings +from app.region_scope import normalize_region_scope from app.repository import AppSettingsRepository logger = logging.getLogger(__name__) @@ -123,8 +124,7 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings: # Flood scope flood_scope_changed = False if update.flood_scope is not None: - stripped = update.flood_scope.strip() - kwargs["flood_scope"] = stripped + kwargs["flood_scope"] = normalize_region_scope(update.flood_scope) flood_scope_changed = True if kwargs: diff --git a/frontend/src/components/ChatHeader.tsx b/frontend/src/components/ChatHeader.tsx index e57fad9..5d7497d 100644 --- a/frontend/src/components/ChatHeader.tsx +++ b/frontend/src/components/ChatHeader.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { toast } from './ui/sonner'; import { isFavorite } from '../utils/favorites'; import { handleKeyboardActivate } from '../utils/a11y'; +import { stripRegionScopePrefix } from '../utils/regionScope'; import { ContactAvatar } from './ContactAvatar'; import { ContactStatusInfo } from './ContactStatusInfo'; import type { Channel, Contact, Conversation, Favorite, RadioConfig } from '../types'; @@ -54,8 +55,8 @@ export function ChatHeader({ const handleEditFloodScopeOverride = () => { if (conversation.type !== 'channel' || !onSetChannelFloodScopeOverride) return; const nextValue = window.prompt( - 'Enter regional override flood scope for this room. This temporarily changes the radio flood scope before send and restores it after, which significantly slows room sends. Leave blank to clear.\n\nNote: some radio clients (including the official ones) implicitly add a "#" character to the beginning of a region specifier. That needs to be manually added here, so if you use the official app with region "Anytown", you should enter it here as "#Anytown".', - activeChannel?.flood_scope_override ?? '' + 'Enter regional override flood scope for this room. This temporarily changes the radio flood scope before send and restores it after, which significantly slows room sends. Leave blank to clear.', + stripRegionScopePrefix(activeChannel?.flood_scope_override) ); if (nextValue === null) return; onSetChannelFloodScopeOverride(conversation.id, nextValue); @@ -140,7 +141,7 @@ export function ChatHeader({ )} {conversation.type === 'channel' && activeChannel?.flood_scope_override && ( - Regional override active: {activeChannel.flood_scope_override} + Regional override active: {stripRegionScopePrefix(activeChannel.flood_scope_override)} )} {conversation.type === 'contact' && diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index 14f3ac7..35b748b 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -5,6 +5,7 @@ import { Button } from '../ui/button'; import { Separator } from '../ui/separator'; import { toast } from '../ui/sonner'; import { RADIO_PRESETS } from '../../utils/radioPresets'; +import { stripRegionScopePrefix } from '../../utils/regionScope'; import type { AppSettings, AppSettingsUpdate, @@ -83,7 +84,7 @@ export function SettingsRadioSection({ useEffect(() => { setAdvertIntervalHours(String(Math.round(appSettings.advert_interval / 3600))); - setFloodScope(appSettings.flood_scope); + setFloodScope(stripRegionScopePrefix(appSettings.flood_scope)); setMaxRadioContacts(String(appSettings.max_radio_contacts)); }, [appSettings]); @@ -256,7 +257,7 @@ export function SettingsRadioSection({ if (newAdvertInterval !== appSettings.advert_interval) { update.advert_interval = newAdvertInterval; } - if (floodScope !== appSettings.flood_scope) { + if (floodScope !== stripRegionScopePrefix(appSettings.flood_scope)) { update.flood_scope = floodScope; } const newMaxRadioContacts = parseInt(maxRadioContacts, 10); @@ -554,10 +555,10 @@ export function SettingsRadioSection({ id="flood-scope" value={floodScope} onChange={(e) => setFloodScope(e.target.value)} - placeholder="#MyRegion" + placeholder="MyRegion" />
- Tag outgoing flood messages with a region name (e.g. #MyRegion). Repeaters configured for + Tag outgoing flood 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.
diff --git a/frontend/src/test/chatHeaderKeyVisibility.test.tsx b/frontend/src/test/chatHeaderKeyVisibility.test.tsx index b1e4231..04952dc 100644 --- a/frontend/src/test/chatHeaderKeyVisibility.test.tsx +++ b/frontend/src/test/chatHeaderKeyVisibility.test.tsx @@ -117,7 +117,7 @@ describe('ChatHeader key visibility', () => { render(