Add cracker dropin interface

This commit is contained in:
Jack Kingsman
2026-01-07 17:35:22 -08:00
parent e33275a2fc
commit 89c5fb2ce4
8 changed files with 612 additions and 568 deletions
+32 -22
View File
@@ -20,6 +20,7 @@ import {
type ConversationTimes,
} from './utils/conversationState';
import { pubkeysMatch, getContactDisplayName } from './utils/pubkey';
import { cn } from '@/lib/utils';
import type {
AppSettings,
AppSettingsUpdate,
@@ -59,6 +60,8 @@ export function App() {
const [showConfig, setShowConfig] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [undecryptedCount, setUndecryptedCount] = useState(0);
const [showCracker, setShowCracker] = useState(false);
const [crackerRunning, setCrackerRunning] = useState(false);
// Track last message times (persisted in localStorage, used for sorting)
const [lastMessageTimes, setLastMessageTimes] = useState<ConversationTimes>(getLastMessageTimes);
// Track unread counts (calculated on load and incremented during session)
@@ -634,6 +637,9 @@ export function App() {
}}
lastMessageTimes={lastMessageTimes}
unreadCounts={unreadCounts}
showCracker={showCracker}
crackerRunning={crackerRunning}
onToggleCracker={() => setShowCracker((prev) => !prev)}
/>
);
@@ -670,28 +676,8 @@ export function App() {
activeConversation.type === 'raw' ? (
<>
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium">Raw Packet Feed</div>
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-hidden min-h-0">
<RawPacketList packets={rawPackets} />
</div>
<div className="h-[280px] flex-shrink-0 border-t border-border overflow-hidden">
<CrackerPanel
packets={rawPackets}
channels={channels}
onChannelCreate={async (name, key) => {
// Create channel without navigating to it
const created = await api.createChannel(name, key);
const data = await api.getChannels();
setChannels(data);
// Try to decrypt historical packets with this key
await api.decryptHistoricalPackets({
key_type: 'channel',
channel_key: created.key,
});
fetchUndecryptedCount();
}}
/>
</div>
<div className="flex-1 overflow-hidden">
<RawPacketList packets={rawPackets} />
</div>
</>
) : (
@@ -750,6 +736,30 @@ export function App() {
</div>
</div>
{/* Global Cracker Panel - always rendered to maintain state */}
<div
className={cn(
"border-t border-border bg-background transition-all duration-200 overflow-hidden",
showCracker ? "h-[275px]" : "h-0"
)}
>
<CrackerPanel
packets={rawPackets}
channels={channels}
onChannelCreate={async (name, key) => {
const created = await api.createChannel(name, key);
const data = await api.getChannels();
setChannels(data);
await api.decryptHistoricalPackets({
key_type: 'channel',
channel_key: created.key,
});
fetchUndecryptedCount();
}}
onRunningChange={setCrackerRunning}
/>
</div>
<NewMessageModal
open={showNewMessage}
contacts={contacts}
+7 -1
View File
@@ -23,9 +23,10 @@ interface CrackerPanelProps {
packets: RawPacket[];
channels: Channel[];
onChannelCreate: (name: string, key: string) => Promise<void>;
onRunningChange?: (running: boolean) => void;
}
export function CrackerPanel({ packets, channels, onChannelCreate }: CrackerPanelProps) {
export function CrackerPanel({ packets, channels, onChannelCreate, onRunningChange }: CrackerPanelProps) {
const [isRunning, setIsRunning] = useState(false);
const [maxLength, setMaxLength] = useState(6);
const [retryFailedAtNextLength, setRetryFailedAtNextLength] = useState(false);
@@ -113,6 +114,11 @@ export function CrackerPanel({ packets, channels, onChannelCreate }: CrackerPane
maxLengthRef.current = maxLength;
}, [maxLength]);
// Notify parent of running state changes
useEffect(() => {
onRunningChange?.(isRunning);
}, [isRunning, onRunningChange]);
// Stats (cracking count is implicit - if progress is shown, we're cracking one)
const pendingCount = Array.from(queue.values()).filter(q => q.status === 'pending').length;
const crackedCount = Array.from(queue.values()).filter(q => q.status === 'cracked').length;
+28
View File
@@ -18,6 +18,9 @@ interface SidebarProps {
onNewMessage: () => void;
lastMessageTimes: ConversationTimes;
unreadCounts: Record<string, number>;
showCracker: boolean;
crackerRunning: boolean;
onToggleCracker: () => void;
}
// Load sort preference from localStorage
@@ -47,6 +50,9 @@ export function Sidebar({
onNewMessage,
lastMessageTimes,
unreadCounts,
showCracker,
crackerRunning,
onToggleCracker,
}: SidebarProps) {
const [sortOrder, setSortOrder] = useState<SortOrder>(loadSortOrder);
const [searchQuery, setSearchQuery] = useState('');
@@ -219,6 +225,28 @@ export function Sidebar({
</div>
)}
{/* Cracker Toggle */}
{!query && (
<div
className={cn(
"px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent",
showCracker && "bg-accent border-l-primary"
)}
onClick={onToggleCracker}
>
<span className="text-muted-foreground text-xs">🔓</span>
<span className="flex-1 truncate">
{showCracker ? 'Hide' : 'Show'} Cracker
<span className={cn(
"ml-1 text-xs",
crackerRunning ? "text-green-500" : "text-muted-foreground"
)}>
({crackerRunning ? 'running' : 'stopped'})
</span>
</span>
</div>
)}
{/* Channels */}
{filteredChannels.length > 0 && (
<>