mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Complete room -> channel rename
This commit is contained in:
@@ -45,8 +45,8 @@ export function ChannelFloodScopeOverrideModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Regional Override</DialogTitle>
|
||||
<DialogDescription>
|
||||
Room-level regional routing temporarily changes the radio flood scope before send and
|
||||
restores it after. This can noticeably slow room sends.
|
||||
Channel-level regional routing temporarily changes the radio flood scope before send and
|
||||
restores it after. This can noticeably slow channel sends.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -201,7 +201,9 @@ export function ChatHeader({
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(conversation.id);
|
||||
toast.success(
|
||||
conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!'
|
||||
conversation.type === 'channel'
|
||||
? 'Channel key copied!'
|
||||
: 'Contact key copied!'
|
||||
);
|
||||
}}
|
||||
title="Click to copy"
|
||||
|
||||
@@ -242,8 +242,8 @@ export function ContactInfoPane({
|
||||
|
||||
<ActivityChartsSection analytics={analytics} />
|
||||
|
||||
<MostActiveRoomsSection
|
||||
rooms={analytics?.most_active_rooms ?? []}
|
||||
<MostActiveChannelsSection
|
||||
channels={analytics?.most_active_rooms ?? []}
|
||||
onNavigateToChannel={onNavigateToChannel}
|
||||
/>
|
||||
</div>
|
||||
@@ -515,8 +515,8 @@ export function ContactInfoPane({
|
||||
|
||||
<ActivityChartsSection analytics={analytics} />
|
||||
|
||||
<MostActiveRoomsSection
|
||||
rooms={analytics?.most_active_rooms ?? []}
|
||||
<MostActiveChannelsSection
|
||||
channels={analytics?.most_active_rooms ?? []}
|
||||
onNavigateToChannel={onNavigateToChannel}
|
||||
/>
|
||||
</div>
|
||||
@@ -588,23 +588,23 @@ function MessageStatsSection({
|
||||
);
|
||||
}
|
||||
|
||||
function MostActiveRoomsSection({
|
||||
rooms,
|
||||
function MostActiveChannelsSection({
|
||||
channels,
|
||||
onNavigateToChannel,
|
||||
}: {
|
||||
rooms: ContactActiveRoom[];
|
||||
channels: ContactActiveRoom[];
|
||||
onNavigateToChannel?: (channelKey: string) => void;
|
||||
}) {
|
||||
if (rooms.length === 0) {
|
||||
if (channels.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-5 py-3 border-b border-border">
|
||||
<SectionLabel>Most Active Rooms</SectionLabel>
|
||||
<SectionLabel>Most Active Channels</SectionLabel>
|
||||
<div className="space-y-1">
|
||||
{rooms.map((room) => (
|
||||
<div key={room.channel_key} className="flex justify-between items-center text-sm">
|
||||
{channels.map((channel) => (
|
||||
<div key={channel.channel_key} className="flex justify-between items-center text-sm">
|
||||
<span
|
||||
className={
|
||||
onNavigateToChannel
|
||||
@@ -614,15 +614,15 @@ function MostActiveRoomsSection({
|
||||
role={onNavigateToChannel ? 'button' : undefined}
|
||||
tabIndex={onNavigateToChannel ? 0 : undefined}
|
||||
onKeyDown={onNavigateToChannel ? handleKeyboardActivate : undefined}
|
||||
onClick={() => onNavigateToChannel?.(room.channel_key)}
|
||||
onClick={() => onNavigateToChannel?.(channel.channel_key)}
|
||||
>
|
||||
{room.channel_name.startsWith('#') || isPublicChannelKey(room.channel_key)
|
||||
? room.channel_name
|
||||
: `#${room.channel_name}`}
|
||||
{channel.channel_name.startsWith('#') || isPublicChannelKey(channel.channel_key)
|
||||
? channel.channel_name
|
||||
: `#${channel.channel_name}`}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground flex-shrink-0 ml-2">
|
||||
{room.message_count.toLocaleString()} msg
|
||||
{room.message_count !== 1 ? 's' : ''}
|
||||
{channel.message_count.toLocaleString()} msg
|
||||
{channel.message_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { toast } from './ui/sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { extractPacketPayloadHex } from '../utils/pathUtils';
|
||||
|
||||
interface CrackedRoom {
|
||||
roomName: string;
|
||||
interface CrackedChannel {
|
||||
channelName: string;
|
||||
key: string;
|
||||
packetId: number;
|
||||
message: string;
|
||||
@@ -45,7 +45,7 @@ export function CrackerPanel({
|
||||
const [twoWordMode, setTwoWordMode] = useState(false);
|
||||
const [progress, setProgress] = useState<ProgressReport | null>(null);
|
||||
const [queue, setQueue] = useState<Map<number, QueueItem>>(new Map());
|
||||
const [crackedRooms, setCrackedRooms] = useState<CrackedRoom[]>([]);
|
||||
const [crackedChannels, setCrackedChannels] = useState<CrackedChannel[]>([]);
|
||||
const [wordlistLoaded, setWordlistLoaded] = useState(false);
|
||||
const [gpuAvailable, setGpuAvailable] = useState<boolean | null>(null);
|
||||
const [undecryptedPacketCount, setUndecryptedPacketCount] = useState<number | null>(null);
|
||||
@@ -325,14 +325,14 @@ export function CrackerPanel({
|
||||
return updated;
|
||||
});
|
||||
|
||||
const newRoom: CrackedRoom = {
|
||||
roomName: result.roomName,
|
||||
const newCracked: CrackedChannel = {
|
||||
channelName: result.roomName,
|
||||
key: result.key,
|
||||
packetId: nextId!,
|
||||
message: result.decryptedMessage || '',
|
||||
crackedAt: Date.now(),
|
||||
};
|
||||
setCrackedRooms((prev) => [...prev, newRoom]);
|
||||
setCrackedChannels((prev) => [...prev, newCracked]);
|
||||
|
||||
// Auto-add channel if not already exists
|
||||
const keyUpper = result.key.toUpperCase();
|
||||
@@ -580,20 +580,20 @@ export function CrackerPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cracked rooms list */}
|
||||
{crackedRooms.length > 0 && (
|
||||
{/* Cracked channels list */}
|
||||
{crackedChannels.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Cracked Rooms:</div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Cracked Channels:</div>
|
||||
<div className="space-y-1">
|
||||
{crackedRooms.map((room, i) => (
|
||||
{crackedChannels.map((channel, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-sm bg-success/10 border border-success/20 rounded px-2 py-1"
|
||||
>
|
||||
<span className="text-success font-medium">#{room.roomName}</span>
|
||||
<span className="text-success font-medium">#{channel.channelName}</span>
|
||||
<span className="text-muted-foreground ml-2 text-xs">
|
||||
"{room.message.slice(0, 50)}
|
||||
{room.message.length > 50 ? '...' : ''}"
|
||||
"{channel.message.slice(0, 50)}
|
||||
{channel.message.length > 50 ? '...' : ''}"
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -604,8 +604,8 @@ export function CrackerPanel({
|
||||
<hr className="border-border" />
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
For unknown-keyed GroupText packets, this will attempt to dictionary attack, then brute
|
||||
force payloads as they arrive, testing room names up to the specified length to discover
|
||||
active rooms on the local mesh (GroupText packets may not be hashtag messages; we have no
|
||||
force payloads as they arrive, testing channel names up to the specified length to discover
|
||||
active channels on the local mesh (GroupText packets may not be hashtag messages; we have no
|
||||
way of knowing but try as if they are).
|
||||
<strong> Retry failed at n+1</strong> will let the cracker return to the failed queue and
|
||||
pick up messages it couldn't crack, attempting them at one longer length.
|
||||
@@ -613,8 +613,8 @@ export function CrackerPanel({
|
||||
concatenated together (e.g. "hello" + "world" = "#helloworld") after the single-word
|
||||
dictionary pass; this can substantially increase search time and also result in
|
||||
false-positives.
|
||||
<strong> Decrypt historical</strong> will run an async job on any room name it finds to see
|
||||
if any historically captured packets will decrypt with that key.
|
||||
<strong> Decrypt historical</strong> will run an async job on any channel name it finds to
|
||||
see if any historically captured packets will decrypt with that key.
|
||||
<strong> Turbo mode</strong> will push your GPU to the max (target dispatch time of 10s) and
|
||||
may allow accelerated cracking and/or system instability.
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Checkbox } from './ui/checkbox';
|
||||
import { Button } from './ui/button';
|
||||
import { toast } from './ui/sonner';
|
||||
|
||||
type Tab = 'new-contact' | 'new-room' | 'hashtag';
|
||||
type Tab = 'new-contact' | 'new-channel' | 'hashtag';
|
||||
|
||||
interface NewMessageModalProps {
|
||||
open: boolean;
|
||||
@@ -37,7 +37,7 @@ export function NewMessageModal({
|
||||
const [tab, setTab] = useState<Tab>('new-contact');
|
||||
const [name, setName] = useState('');
|
||||
const [contactKey, setContactKey] = useState('');
|
||||
const [roomKey, setRoomKey] = useState('');
|
||||
const [channelKey, setChannelKey] = useState('');
|
||||
const [tryHistorical, setTryHistorical] = useState(false);
|
||||
const [permitCapitals, setPermitCapitals] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -47,7 +47,7 @@ export function NewMessageModal({
|
||||
const resetForm = () => {
|
||||
setName('');
|
||||
setContactKey('');
|
||||
setRoomKey('');
|
||||
setChannelKey('');
|
||||
setTryHistorical(false);
|
||||
setPermitCapitals(false);
|
||||
setError('');
|
||||
@@ -65,12 +65,12 @@ export function NewMessageModal({
|
||||
}
|
||||
// handleCreateContact sets activeConversation with the backend-normalized key
|
||||
await onCreateContact(name.trim(), contactKey.trim(), tryHistorical);
|
||||
} else if (tab === 'new-room') {
|
||||
if (!name.trim() || !roomKey.trim()) {
|
||||
setError('Room name and key are required');
|
||||
} else if (tab === 'new-channel') {
|
||||
if (!name.trim() || !channelKey.trim()) {
|
||||
setError('Channel name and key are required');
|
||||
return;
|
||||
}
|
||||
await onCreateChannel(name.trim(), roomKey.trim(), tryHistorical);
|
||||
await onCreateChannel(name.trim(), channelKey.trim(), tryHistorical);
|
||||
} else if (tab === 'hashtag') {
|
||||
const channelName = name.trim();
|
||||
const validationError = validateHashtagName(channelName);
|
||||
@@ -147,7 +147,7 @@ export function NewMessageModal({
|
||||
<DialogTitle>New Conversation</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{tab === 'new-contact' && 'Add a new contact by entering their name and public key'}
|
||||
{tab === 'new-room' && 'Create a private room with a shared encryption key'}
|
||||
{tab === 'new-channel' && 'Create a private channel with a shared encryption key'}
|
||||
{tab === 'hashtag' && 'Join a public hashtag channel'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -162,7 +162,7 @@ export function NewMessageModal({
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="new-contact">Contact</TabsTrigger>
|
||||
<TabsTrigger value="new-room">Private Channel</TabsTrigger>
|
||||
<TabsTrigger value="new-channel">Private Channel</TabsTrigger>
|
||||
<TabsTrigger value="hashtag">Hashtag Channel</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -187,23 +187,23 @@ export function NewMessageModal({
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="new-room" className="mt-4 space-y-4">
|
||||
<TabsContent value="new-channel" className="mt-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="room-name">Room Name</Label>
|
||||
<Label htmlFor="channel-name">Channel Name</Label>
|
||||
<Input
|
||||
id="room-name"
|
||||
id="channel-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Room name"
|
||||
placeholder="Channel name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="room-key">Room Key</Label>
|
||||
<Label htmlFor="channel-key">Channel Key</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="room-key"
|
||||
value={roomKey}
|
||||
onChange={(e) => setRoomKey(e.target.value)}
|
||||
id="channel-key"
|
||||
value={channelKey}
|
||||
onChange={(e) => setChannelKey(e.target.value)}
|
||||
placeholder="Pre-shared key (hex)"
|
||||
className="flex-1"
|
||||
/>
|
||||
@@ -217,7 +217,7 @@ export function NewMessageModal({
|
||||
const hex = Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
setRoomKey(hex);
|
||||
setChannelKey(hex);
|
||||
}}
|
||||
title="Generate random key"
|
||||
aria-label="Generate random key"
|
||||
@@ -251,7 +251,7 @@ export function NewMessageModal({
|
||||
onChange={(e) => setPermitCapitals(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-input accent-primary"
|
||||
/>
|
||||
<span className="text-sm">Permit capitals in room key derivation</span>
|
||||
<span className="text-sm">Permit capitals in channel key derivation</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground pl-7">
|
||||
Not recommended; most companions normalize to lowercase
|
||||
|
||||
@@ -161,7 +161,7 @@ function buildGroupTextResolutionCandidates(channels: Channel[]): GroupTextResol
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveGroupTextRoomName(
|
||||
function resolveGroupTextChannelName(
|
||||
payload: {
|
||||
channelHash?: string;
|
||||
cipherMac?: string;
|
||||
@@ -211,15 +211,15 @@ function getPacketContext(
|
||||
groupTextCandidates: GroupTextResolutionCandidate[]
|
||||
) {
|
||||
const fallbackSender = packet.decrypted_info?.sender ?? null;
|
||||
const fallbackRoom = packet.decrypted_info?.channel_name ?? null;
|
||||
const fallbackChannel = packet.decrypted_info?.channel_name ?? null;
|
||||
|
||||
if (!inspection.decoded?.payload.decoded) {
|
||||
if (!fallbackSender && !fallbackRoom) {
|
||||
if (!fallbackSender && !fallbackChannel) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
title: fallbackRoom ? 'Room' : 'Context',
|
||||
primary: fallbackRoom ?? 'Sender metadata available',
|
||||
title: fallbackChannel ? 'Channel' : 'Context',
|
||||
primary: fallbackChannel ?? 'Sender metadata available',
|
||||
secondary: fallbackSender ? `Sender: ${fallbackSender}` : null,
|
||||
};
|
||||
}
|
||||
@@ -231,11 +231,12 @@ function getPacketContext(
|
||||
ciphertext?: string;
|
||||
decrypted?: { sender?: string; message?: string };
|
||||
};
|
||||
const roomName = fallbackRoom ?? resolveGroupTextRoomName(payload, groupTextCandidates);
|
||||
const channelName =
|
||||
fallbackChannel ?? resolveGroupTextChannelName(payload, groupTextCandidates);
|
||||
return {
|
||||
title: roomName ? 'Room' : 'Channel',
|
||||
title: 'Channel',
|
||||
primary:
|
||||
roomName ?? (payload.channelHash ? `Channel hash ${payload.channelHash}` : 'GroupText'),
|
||||
channelName ?? (payload.channelHash ? `Channel hash ${payload.channelHash}` : 'GroupText'),
|
||||
secondary: payload.decrypted?.sender
|
||||
? `Sender: ${payload.decrypted.sender}`
|
||||
: fallbackSender
|
||||
|
||||
@@ -748,7 +748,7 @@ export function Sidebar({
|
||||
icon: <LockOpen className="h-4 w-4" />,
|
||||
label: (
|
||||
<>
|
||||
{showCracker ? 'Hide' : 'Show'} Room Finder
|
||||
{showCracker ? 'Hide' : 'Show'} Channel Finder
|
||||
<span
|
||||
className={cn(
|
||||
'ml-1 text-[11px]',
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('ContactInfoPane', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('loads name-only channel stats and most active rooms', async () => {
|
||||
it('loads name-only channel stats and most active channels', async () => {
|
||||
getContactAnalytics.mockResolvedValue(
|
||||
createAnalytics(null, {
|
||||
lookup_type: 'name',
|
||||
@@ -188,7 +188,7 @@ describe('ContactInfoPane', () => {
|
||||
expect(screen.getByText('Name First In Use')).toBeInTheDocument();
|
||||
expect(screen.getByText('Messages Per Hour')).toBeInTheDocument();
|
||||
expect(screen.getByText('Messages Per Week')).toBeInTheDocument();
|
||||
expect(screen.getByText('Most Active Rooms')).toBeInTheDocument();
|
||||
expect(screen.getByText('Most Active Channels')).toBeInTheDocument();
|
||||
expect(screen.getByText('#ops')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Name-only analytics include channel messages only/i)
|
||||
|
||||
@@ -105,13 +105,13 @@ describe('NewMessageModal form reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('new-room tab', () => {
|
||||
describe('new-channel tab', () => {
|
||||
it('clears name and key after successful Create', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
await switchToTab(user, 'Private Channel');
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom');
|
||||
await user.type(screen.getByPlaceholderText('Channel name'), 'MyRoom');
|
||||
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16));
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create' }));
|
||||
@@ -128,7 +128,7 @@ describe('NewMessageModal form reset', () => {
|
||||
renderModal();
|
||||
await switchToTab(user, 'Private Channel');
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom');
|
||||
await user.type(screen.getByPlaceholderText('Channel name'), 'MyRoom');
|
||||
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16));
|
||||
await user.click(screen.getByRole('button', { name: 'Create' }));
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('NewMessageModal form reset', () => {
|
||||
});
|
||||
|
||||
describe('tab switching resets form', () => {
|
||||
it('clears contact fields when switching to room tab', async () => {
|
||||
it('clears contact fields when switching to channel tab', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
await switchToTab(user, 'Contact');
|
||||
@@ -153,18 +153,18 @@ describe('NewMessageModal form reset', () => {
|
||||
// Switch to Private Channel tab — fields should reset
|
||||
await switchToTab(user, 'Private Channel');
|
||||
|
||||
expect((screen.getByPlaceholderText('Room name') as HTMLInputElement).value).toBe('');
|
||||
expect((screen.getByPlaceholderText('Channel name') as HTMLInputElement).value).toBe('');
|
||||
expect((screen.getByPlaceholderText('Pre-shared key (hex)') as HTMLInputElement).value).toBe(
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('clears room fields when switching to hashtag tab', async () => {
|
||||
it('clears channel fields when switching to hashtag tab', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderModal();
|
||||
await switchToTab(user, 'Private Channel');
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Room name'), 'SecretRoom');
|
||||
await user.type(screen.getByPlaceholderText('Channel name'), 'SecretRoom');
|
||||
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'ff'.repeat(16));
|
||||
|
||||
await switchToTab(user, 'Hashtag Channel');
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('RawPacketFeedView', () => {
|
||||
expect(screen.queryByText('Identity not resolvable')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens a packet detail modal from the raw feed and decrypts room messages when a key is loaded', () => {
|
||||
it('opens a packet detail modal from the raw feed and decrypts channel messages when a key is loaded', () => {
|
||||
renderView({
|
||||
packets: [
|
||||
{
|
||||
@@ -392,7 +392,7 @@ describe('RawPacketFeedView', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not guess a room name when multiple loaded channels collide on the group hash', () => {
|
||||
it('does not guess a channel name when multiple loaded channels collide on the group hash', () => {
|
||||
renderView({
|
||||
packets: [
|
||||
{
|
||||
|
||||
@@ -67,8 +67,8 @@ export function describeCiphertextStructure(
|
||||
case PayloadType.GroupText:
|
||||
return `Encrypted message content (${byteLength} bytes). Contains encrypted plaintext with this structure:
|
||||
• Timestamp (4 bytes) - send time as unix timestamp
|
||||
• Flags (1 byte) - room-message flags byte
|
||||
• Message (remaining bytes) - UTF-8 room message text`;
|
||||
• Flags (1 byte) - channel-message flags byte
|
||||
• Message (remaining bytes) - UTF-8 channel message text`;
|
||||
case PayloadType.TextMessage:
|
||||
return `Encrypted message data (${byteLength} bytes). Contains encrypted plaintext with this structure:
|
||||
• Timestamp (4 bytes) - send time as unix timestamp
|
||||
|
||||
Reference in New Issue
Block a user