import type { WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { format } from 'date-fns'; import { hexy } from 'hexy'; import { useAtomValue } from 'jotai'; import { useMemo, useRef, useState } from 'react'; import { useFormatText } from '../hooks/useFormatText'; import { activeWebsocketConnectionAtom, activeWebsocketConnectionsAtom, setPinnedWebsocketConnectionId, useWebsocketEvents, } from '../hooks/usePinnedWebsocketConnection'; import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { languageFromContentType } from '../lib/contentType'; import { copyToClipboard } from '../lib/copy'; import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Editor } from './core/Editor/LazyEditor'; import { HotKeyList } from './core/HotKeyList'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { LoadingIcon } from './core/LoadingIcon'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { HStack, VStack } from './core/Stacks'; import { WebsocketStatusTag } from './core/WebsocketStatusTag'; import { EmptyStateText } from './EmptyStateText'; import { ErrorBoundary } from './ErrorBoundary'; import { RecentWebsocketConnectionsDropdown } from './RecentWebsocketConnectionsDropdown'; interface Props { activeRequest: WebsocketRequest; } export function WebsocketResponsePane({ activeRequest }: Props) { const [activeEventId, setActiveEventId] = useState(null); const [showLarge, setShowLarge] = useStateWithDeps(false, [activeRequest.id]); const [showingLarge, setShowingLarge] = useState(false); const [hexDumps, setHexDumps] = useState>({}); const activeConnection = useAtomValue(activeWebsocketConnectionAtom); const connections = useAtomValue(activeWebsocketConnectionsAtom); const events = useWebsocketEvents(activeConnection?.id ?? null); const activeEvent = useMemo( () => events.find((m) => m.id === activeEventId) ?? null, [activeEventId, events], ); const hexDump = hexDumps[activeEventId ?? 'n/a'] ?? activeEvent?.messageType === 'binary'; const message = useMemo(() => { if (hexDump) { return activeEvent?.message ? hexy(activeEvent?.message) : ''; } return activeEvent?.message ? new TextDecoder('utf-8').decode(Uint8Array.from(activeEvent.message)) : ''; }, [activeEvent?.message, hexDump]); const language = languageFromContentType(null, message); const formattedMessage = useFormatText({ language, text: message, pretty: true }); return ( activeConnection == null ? ( ) : (
{activeConnection.state !== 'closed' && ( )} {events.length} Messages {activeConnection.error} ) } render={(event) => ( { if (event.id === activeEventId) setActiveEventId(null); else setActiveEventId(event.id); }} /> )} />
) } secondSlot={ activeEvent != null && activeConnection != null ? () => (
{activeEvent.messageType === 'close' ? 'Connection Closed' : activeEvent.messageType === 'open' ? 'Connection open' : `Message ${activeEvent.isServer ? 'Received' : 'Sent'}`}
{message !== '' && ( copyToClipboard(formattedMessage ?? '')} /> )}
{!showLarge && activeEvent.message.length > 1000 * 1000 ? ( Message previews larger than 1MB are hidden
) : activeEvent.message.length === 0 ? ( No Content ) : ( )}
) : null } /> ); } function EventRow({ onClick, isActive, event, }: { onClick?: () => void; isActive?: boolean; event: WebsocketEvent; }) { const { createdAt, message: messageBytes, isServer, messageType } = event; const ref = useRef(null); const message = messageBytes ? new TextDecoder('utf-8').decode(Uint8Array.from(messageBytes)) : ''; return (
); }