├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── 1280x800-2.png ├── 1280x800.png ├── 1400x560.png └── 440x280.png ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── background.js ├── icon.png ├── icon.svg ├── index.html └── manifest.json ├── src ├── App.tsx ├── components │ ├── ConnectionPanel.tsx │ ├── HistoryPanel │ │ ├── MessageHistory.tsx │ │ ├── Messages.tsx │ │ ├── UrlHistory.tsx │ │ └── index.tsx │ ├── MessageHistory.tsx │ └── MessageInput.tsx ├── hooks │ ├── useStorageState.ts │ ├── useTheme.ts │ └── useWebSocket.ts ├── index.css ├── index.tsx ├── types │ └── message.ts └── utils │ ├── formatters.ts │ └── jsonFormatter.ts ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | build 132 | build.zip 133 | .DS_Store 134 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.2] - 2025-01-10 4 | 5 | ### Added 6 | - GitHub repository link with star count in the header 7 | - Version number display in the header 8 | 9 | ### Changed 10 | - Improved UI styling for protocol selector dropdown 11 | - Reduced font size in message input area for better readability 12 | - Removed default WebSocket/Socket.IO URLs for better security 13 | - Updated header layout with better alignment and spacing 14 | - Enhanced dropdown menu with modern styling and custom arrow 15 | 16 | ### Fixed 17 | - Protocol switching no longer automatically sets predefined URLs 18 | - Better handling of connection state when switching protocols 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 oslook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Websocket client icon 3 |

Websocket Client Extension

4 |

Quick WebSocket Client with Socket.IO

5 |
6 | 7 | 8 | # [ ⏬ Install to Chrome](https://chromewebstore.google.com/detail/quick-websocket-client-wi/enmpedlkjjjnhoehlkkghdjiloebecpn) 9 | 10 | A powerful and user-friendly WebSocket testing client for developers. 11 | 12 | ## Features 13 | 14 | - 🔌 Easy WebSocket Connection Management 15 | - Quick connect/disconnect with WebSocket endpoints 16 | - Connection status monitoring 17 | - Detailed connection state logging 18 | 19 | - 📝 Advanced Message Handling 20 | - Support for both text and binary messages 21 | - Built-in JSON formatting and validation 22 | - Message history with timestamp and direction 23 | - Save frequently used messages for quick access 24 | 25 | - 💾 Persistent Storage 26 | - Auto-save connection history 27 | - Save favorite messages for reuse 28 | - Quick access to recent connections 29 | 30 | - 🎨 Modern User Interface 31 | - Clean and intuitive design 32 | - Real-time message updates 33 | - Compact message display 34 | - Dark/Light theme support 35 | 36 | ## Perfect for 37 | 38 | - Web developers testing WebSocket APIs 39 | - Backend developers debugging WebSocket services 40 | - QA engineers validating WebSocket implementations 41 | - Anyone working with WebSocket-based applications 42 | 43 | ## How to Use 44 | 45 | 1. Enter your WebSocket URL (e.g., ws://localhost:8080) 46 | 2. Click "Connect" to establish connection 47 | 3. Send text or binary messages 48 | 4. View real-time responses 49 | 5. Save frequently used messages for quick access 50 | 6. Monitor connection status and history 51 | 52 | ## Technical Details 53 | 54 | - Built with React and TypeScript 55 | - Uses native WebSocket API 56 | - Supports all modern browsers 57 | - Zero external dependencies for core functionality 58 | - Secure local storage for saved data 59 | 60 | Perfect for developers who need a reliable, feature-rich WebSocket testing tool right in their browser. 61 | 62 | 63 | Test Websoket: wss://ws.postman-echo.com/raw 64 | Test Socket.IO: https://ws.postman-echo.com/socketio 65 | 66 | -------------------------------------------------------------------------------- /assets/1280x800-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1280x800-2.png -------------------------------------------------------------------------------- /assets/1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1280x800.png -------------------------------------------------------------------------------- /assets/1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/1400x560.png -------------------------------------------------------------------------------- /assets/440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/assets/440x280.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-client-extension", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "INLINE_RUNTIME_CHUNK=false react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "date-fns": "^4.1.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-icons": "^5.4.0", 16 | "react-scripts": "5.0.1", 17 | "socket.io-client": "^4.7.4", 18 | "typescript": "^4.9.5" 19 | }, 20 | "devDependencies": { 21 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 22 | "@types/chrome": "^0.0.254", 23 | "@types/react": "^18.2.0", 24 | "@types/react-dom": "^18.2.0", 25 | "autoprefixer": "^10.4.14", 26 | "postcss": "^8.4.23", 27 | "tailwindcss": "^3.3.2" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; -------------------------------------------------------------------------------- /public/background.js: -------------------------------------------------------------------------------- 1 | chrome.action.onClicked.addListener(() => { 2 | chrome.tabs.create({ 3 | url: 'index.html' 4 | }); 5 | }); -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslook/quick-websocket-client/acabd84494d884d61b667d9a7542c6a6e38016d7/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quick WebSocket Client 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Quick WebSocket Client with Socket.IO", 4 | "version": "0.0.2.1", 5 | "description": "Construct custom Web Socket requests and handle responses to directly test your Web Socket services,Includes Socket.IO support.", 6 | "action": { 7 | "default_icon": "icon.png" 8 | }, 9 | "background": { 10 | "service_worker": "background.js", 11 | "type": "module" 12 | }, 13 | "permissions": ["storage"], 14 | "icons": { 15 | "48": "icon.png", 16 | "128": "icon.png" 17 | } 18 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useWebSocket } from './hooks/useWebSocket'; 2 | import { useState, useEffect, useRef } from 'react'; 3 | import ConnectionPanel from './components/ConnectionPanel'; 4 | import MessageInput from './components/MessageInput'; 5 | import MessageHistory from './components/MessageHistory'; 6 | import HistoryPanel from './components/HistoryPanel'; 7 | import { useStorageState } from './hooks/useStorageState'; 8 | import { useTheme } from './hooks/useTheme'; 9 | import { SavedMessage, ProtocolType } from './types/message'; 10 | 11 | const App = () => { 12 | const [url, setUrl] = useState(''); 13 | const [protocol, setProtocol] = useState('websocket'); 14 | const [savedMessages, setSavedMessages] = useStorageState('savedMessages', []); 15 | const [subscribedEvents, setSubscribedEvents] = useState>(new Set()); 16 | const unsubscribedEvents = useRef>(new Set()); 17 | const allReceivedEvents = useRef>(new Set()); // Track all unique events for autocomplete 18 | const [githubStars, setGithubStars] = useState(null); 19 | const { isDark, toggleTheme } = useTheme(); 20 | 21 | useEffect(() => { 22 | // Fetch GitHub stars 23 | fetch('https://api.github.com/repos/oslook/quick-websocket-client') 24 | .then(res => res.json()) 25 | .then(data => setGithubStars(data.stargazers_count)) 26 | .catch(console.error); 27 | }, []); 28 | 29 | const { 30 | isConnected, 31 | messages, 32 | error, 33 | connect, 34 | disconnect, 35 | sendMessage, 36 | clearMessages, 37 | } = useWebSocket(); 38 | 39 | // Auto-subscribe to received events 40 | useEffect(() => { 41 | if (protocol === 'socket.io') { 42 | const newEvents = messages 43 | .filter(m => m.direction === 'received' && m.event) 44 | .map(m => m.event as string); 45 | 46 | // Add to all received events for autocomplete 47 | newEvents.forEach(event => allReceivedEvents.current.add(event)); 48 | 49 | // Filter out unsubscribed events and add new ones 50 | const eventsToAdd = newEvents.filter(event => !unsubscribedEvents.current.has(event)); 51 | 52 | if (eventsToAdd.length > 0) { 53 | setSubscribedEvents(prev => new Set([...prev, ...eventsToAdd])); 54 | } 55 | } 56 | }, [messages, protocol]); 57 | 58 | const handleSaveMessage = (message: SavedMessage) => { 59 | if (!savedMessages.some(m => 60 | m.content === message.content && 61 | m.type === message.type && 62 | m.event === message.event 63 | )) { 64 | setSavedMessages([message, ...savedMessages]); 65 | } 66 | }; 67 | 68 | const handleProtocolChange = (newProtocol: ProtocolType) => { 69 | if (isConnected) { 70 | disconnect(); 71 | } 72 | setProtocol(newProtocol); 73 | // if (newProtocol === 'websocket') { 74 | // setUrl('wss://ws.postman-echo.com/raw'); 75 | // } else { 76 | // setUrl('https://ws.postman-echo.com/socketio'); 77 | // } 78 | // Clear all events when switching protocols 79 | setSubscribedEvents(new Set()); 80 | unsubscribedEvents.current.clear(); 81 | allReceivedEvents.current.clear(); 82 | }; 83 | 84 | const handleUnsubscribe = (event: string) => { 85 | setSubscribedEvents(prev => { 86 | const newSet = new Set(prev); 87 | newSet.delete(event); 88 | return newSet; 89 | }); 90 | unsubscribedEvents.current.add(event); 91 | }; 92 | 93 | const handleSubscribe = (event: string) => { 94 | if (!subscribedEvents.has(event)) { 95 | setSubscribedEvents(prev => new Set([...prev, event])); 96 | unsubscribedEvents.current.delete(event); 97 | } 98 | }; 99 | 100 | // Get all available events for autocomplete 101 | const getAvailableEvents = () => { 102 | return Array.from(allReceivedEvents.current) 103 | .filter(event => !subscribedEvents.has(event)); 104 | }; 105 | 106 | return ( 107 |
108 | {/* Left Sidebar */} 109 |
110 | {/* URL History */} 111 |
112 |

Connection History

113 | setUrl(url)} 115 | disabled={isConnected} 116 | /> 117 |
118 | {/* Saved Messages */} 119 |
120 |

Saved Messages

121 | { 124 | setSavedMessages(savedMessages.filter((_, i) => i !== index)); 125 | }} 126 | onSelect={(content: string, type: 'text' | 'binary', event?: string) => isConnected && sendMessage(content, type, event)} 127 | disabled={!isConnected} 128 | /> 129 |
130 |
131 | 132 | {/* Main Content */} 133 |
134 |
135 |
136 |
137 |

Quick WebSocket Client

138 | Including Socket.IO support 139 |
140 |
141 | 142 | v0.0.2.1 143 | {githubStars !== null && ( 144 | 145 | 146 | 147 | 148 | {githubStars} 149 | 150 | )} 151 | 159 | 160 | 175 |
176 |
177 | 187 |
188 |
189 | 195 |
196 |
197 | 207 |
208 |
209 |
210 | ); 211 | }; 212 | 213 | export default App; -------------------------------------------------------------------------------- /src/components/ConnectionPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useStorageState } from '../hooks/useStorageState'; 3 | import { ProtocolType } from '../types/message'; 4 | 5 | type ConnectionPanelProps = { 6 | url: string; 7 | setUrl: (url: string) => void; 8 | onConnect: (url: string) => void; 9 | onDisconnect: () => void; 10 | isConnected: boolean; 11 | error: string | null; 12 | protocol: ProtocolType; 13 | handleProtocolChange: (protocol: ProtocolType) => void; 14 | }; 15 | 16 | const ConnectionPanel = ({ 17 | url, 18 | setUrl, 19 | onConnect, 20 | onDisconnect, 21 | isConnected, 22 | error, 23 | protocol, 24 | handleProtocolChange 25 | }: ConnectionPanelProps) => { 26 | const [urlHistory, setUrlHistory] = useStorageState('urlHistory', []); 27 | 28 | const handleConnect = () => { 29 | if (url.trim()) { 30 | onConnect(url.trim()); 31 | if (!urlHistory.includes(url)) { 32 | setUrlHistory([url, ...urlHistory].slice(0, 10)); 33 | } 34 | } 35 | }; 36 | 37 | return ( 38 |
39 |
40 | 49 | setUrl(e.target.value)} 53 | className="flex-1 px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200" 54 | placeholder={protocol === 'websocket' ? "WebSocket URL (ws:// or wss://)" : "Socket.IO URL (http:// or https://)"} 55 | disabled={isConnected} 56 | /> 57 | 67 |
68 | {error && ( 69 |
70 | {error} 71 |
72 | )} 73 |
74 | ); 75 | }; 76 | 77 | export default ConnectionPanel; -------------------------------------------------------------------------------- /src/components/HistoryPanel/MessageHistory.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageState } from '../../hooks/useStorageState'; 2 | import { SavedMessage } from '../../types/message'; 3 | 4 | type MessageHistoryProps = { 5 | onSelect: (content: string, type: 'text' | 'binary', event?: string) => void; 6 | disabled?: boolean; 7 | messages: SavedMessage[]; 8 | onDeleteMessage: (index: number) => void; 9 | }; 10 | 11 | const MessageHistory = ({ 12 | onSelect, 13 | disabled, 14 | messages, 15 | onDeleteMessage 16 | }: MessageHistoryProps) => { 17 | if (messages.length === 0) { 18 | return ( 19 |
No saved messages
20 | ); 21 | } 22 | 23 | return ( 24 |
25 | {messages.map((message, index) => ( 26 |
30 | 41 | 49 |
50 | ))} 51 |
52 | ); 53 | }; 54 | 55 | export default MessageHistory; -------------------------------------------------------------------------------- /src/components/HistoryPanel/Messages.tsx: -------------------------------------------------------------------------------- 1 | import { SavedMessage, MessageType } from '../../types/message'; 2 | 3 | type MessagesProps = { 4 | messages: SavedMessage[]; 5 | onDeleteMessage: (index: number) => void; 6 | onSelect: (content: string, type: MessageType, event?: string) => void; 7 | disabled: boolean; 8 | }; 9 | 10 | const Messages = ({ messages, onDeleteMessage, onSelect, disabled }: MessagesProps) => { 11 | const handleSelect = (message: SavedMessage) => { 12 | if (!disabled) { 13 | onSelect(message.content, message.type, message.event); 14 | } 15 | }; 16 | 17 | return ( 18 |
19 | {messages.map((message, index) => ( 20 |
24 |
25 | {message.event && ( 26 | {message.event} 27 | )} 28 | {message.content} 29 |
30 |
31 | 38 | 44 |
45 |
46 | ))} 47 | {messages.length === 0 && ( 48 |
49 | No saved messages 50 |
51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Messages; -------------------------------------------------------------------------------- /src/components/HistoryPanel/UrlHistory.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageState } from '../../hooks/useStorageState'; 2 | 3 | type UrlHistoryProps = { 4 | onSelect: (url: string) => void; 5 | disabled?: boolean; 6 | }; 7 | 8 | const UrlHistory = ({ onSelect, disabled }: UrlHistoryProps) => { 9 | const [urlHistory, setUrlHistory] = useStorageState('urlHistory', []); 10 | 11 | const handleDelete = (urlToDelete: string) => { 12 | setUrlHistory(urlHistory.filter(url => url !== urlToDelete)); 13 | }; 14 | 15 | if (urlHistory.length === 0) { 16 | return ( 17 |
No connection history
18 | ); 19 | } 20 | 21 | return ( 22 |
23 | {urlHistory.map((url) => ( 24 |
28 | 36 | 44 |
45 | ))} 46 |
47 | ); 48 | }; 49 | 50 | export default UrlHistory; -------------------------------------------------------------------------------- /src/components/HistoryPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import UrlHistory from './UrlHistory'; 2 | import MessageHistory from './MessageHistory'; 3 | 4 | const HistoryPanel = { 5 | Urls: UrlHistory, 6 | Messages: MessageHistory, 7 | }; 8 | 9 | export default HistoryPanel; -------------------------------------------------------------------------------- /src/components/MessageHistory.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from '../types/message'; 2 | import { format } from 'date-fns'; 3 | import { useEffect, useRef, useState } from 'react'; 4 | import { formatJSON, isJSON, formatHexDump } from '../utils/formatters'; 5 | 6 | type MessageHistoryProps = { 7 | messages: Message[]; 8 | onClear: () => void; 9 | subscribedEvents?: Set; 10 | protocol?: 'websocket' | 'socket.io'; 11 | }; 12 | 13 | type ViewMode = 'text' | 'json' | 'hex'; 14 | 15 | type MessageItemProps = { 16 | message: Message; 17 | className: string; 18 | }; 19 | 20 | const MessageItem = ({ message, className }: MessageItemProps) => { 21 | const [isExpanded, setIsExpanded] = useState(false); 22 | const [viewMode, setViewMode] = useState('text'); 23 | const content = message.content; 24 | const isExpandable = content.includes('\n') || content.length > 80; 25 | const isJSONContent = isJSON(content); 26 | 27 | // Format one-line preview 28 | const getPreviewContent = () => { 29 | if (content.length <= 80) return content; 30 | return content.split('\n')[0].slice(0, 80) + '...'; 31 | }; 32 | 33 | // Format expanded content based on view mode 34 | const getExpandedContent = () => { 35 | switch (viewMode) { 36 | case 'json': 37 | return isJSONContent ? formatJSON(content) : 'Invalid JSON'; 38 | case 'hex': 39 | return formatHexDump(content); 40 | default: 41 | return content; 42 | } 43 | }; 44 | 45 | return ( 46 |
47 |
isExpandable && setIsExpanded(!isExpanded)} 50 | > 51 | 52 | {format(message.timestamp, 'HH:mm:ss.SSS')} 53 | 54 | 55 | {formatMessage(message, getPreviewContent())} 56 | 57 | {isExpandable && ( 58 | 67 | )} 68 |
69 | 70 | {isExpanded && ( 71 |
72 |
73 | 79 | {isJSONContent && ( 80 | 86 | )} 87 | 93 |
94 |
 95 |             {getExpandedContent()}
 96 |           
97 |
98 | )} 99 |
100 | ); 101 | }; 102 | 103 | const MessageHistory = ({ messages, onClear, subscribedEvents = new Set(), protocol = 'websocket' }: MessageHistoryProps) => { 104 | const messagesEndRef = useRef(null); 105 | const scrollContainerRef = useRef(null); 106 | 107 | // Filter messages based on subscribed events 108 | const filteredMessages = messages.filter(message => { 109 | // Always show system/connection messages 110 | if (message.type === 'connection') return true; 111 | 112 | // For Socket.IO, only show messages with subscribed events or sent messages 113 | if (protocol === 'socket.io' && message.event) { 114 | return message.direction === 'sent' || subscribedEvents.has(message.event); 115 | } 116 | 117 | // Show all WebSocket messages 118 | return true; 119 | }); 120 | 121 | // Auto scroll to bottom when new messages arrive 122 | useEffect(() => { 123 | const scrollContainer = scrollContainerRef.current; 124 | if (scrollContainer) { 125 | const { scrollHeight, clientHeight, scrollTop } = scrollContainer; 126 | const isScrolledNearBottom = scrollHeight - clientHeight - scrollTop < 100; 127 | 128 | if (isScrolledNearBottom) { 129 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); 130 | } 131 | } 132 | }, [filteredMessages]); 133 | 134 | const getMessageClass = (message: Message) => { 135 | if (message.type === 'connection') { 136 | switch (message.level) { 137 | case 'success': 138 | return 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800'; 139 | case 'error': 140 | return 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800'; 141 | case 'warning': 142 | return 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800'; 143 | default: 144 | return 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800'; 145 | } 146 | } 147 | return message.direction === 'sent' 148 | ? 'bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800' 149 | : 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700'; 150 | }; 151 | 152 | return ( 153 |
154 |
155 |

Messages

156 | 162 |
163 |
164 | {filteredMessages.map((message, index) => ( 165 | 170 | ))} 171 | {filteredMessages.length === 0 && ( 172 |
173 | No messages yet 174 |
175 | )} 176 |
177 |
178 |
179 | ); 180 | }; 181 | 182 | const formatMessage = (message: Message, content: string) => { 183 | if (message.type === 'connection') { 184 | return content; 185 | } 186 | 187 | let prefix = ''; 188 | if (message.direction === 'sent') { 189 | prefix = message.event 190 | ? `📤 [Emit: ${message.event}] ` 191 | : '📤 [Sent] '; 192 | } else { 193 | prefix = message.event 194 | ? `📥 [Event: ${message.event}] ` 195 | : '📥 [Received] '; 196 | } 197 | 198 | return prefix + content; 199 | }; 200 | 201 | export default MessageHistory; -------------------------------------------------------------------------------- /src/components/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect, useRef } from 'react'; 2 | import { useStorageState } from '../hooks/useStorageState'; 3 | import { formatJSON, isJSON } from '../utils/jsonFormatter'; 4 | import { ProtocolType, SavedMessage, MessageType } from '../types/message'; 5 | 6 | type MessageInputProps = { 7 | onSend: (content: string, type: MessageType, event?: string) => void; 8 | isConnected: boolean; 9 | onSaveMessage: (message: SavedMessage) => void; 10 | protocol: ProtocolType; 11 | subscribedEvents: Set; 12 | onSubscribe: (event: string) => void; 13 | onUnsubscribe: (event: string) => void; 14 | availableEvents?: string[]; 15 | }; 16 | 17 | const MessageInput = ({ 18 | onSend, 19 | isConnected, 20 | onSaveMessage, 21 | protocol, 22 | subscribedEvents, 23 | onSubscribe, 24 | onUnsubscribe, 25 | availableEvents = [] 26 | }: MessageInputProps) => { 27 | const [message, setMessage] = useState(''); 28 | const [type, setType] = useState('text'); 29 | const [event, setEvent] = useState(''); 30 | const [showFormatted, setShowFormatted] = useState(false); 31 | const [newEvent, setNewEvent] = useState(''); 32 | const [showSuggestions, setShowSuggestions] = useState(false); 33 | const suggestionRef = useRef(null); 34 | const inputRef = useRef(null); 35 | 36 | // Handle click outside to close suggestions 37 | useEffect(() => { 38 | const handleClickOutside = (event: MouseEvent) => { 39 | if (suggestionRef.current && inputRef.current && 40 | !suggestionRef.current.contains(event.target as Node) && 41 | !inputRef.current.contains(event.target as Node)) { 42 | setShowSuggestions(false); 43 | } 44 | }; 45 | 46 | document.addEventListener('mousedown', handleClickOutside); 47 | return () => { 48 | document.removeEventListener('mousedown', handleClickOutside); 49 | }; 50 | }, []); 51 | 52 | // Filter suggestions based on input 53 | const suggestions = availableEvents 54 | .filter(e => e.toLowerCase().includes(newEvent.toLowerCase())) 55 | .slice(0, 5); // Limit to 5 suggestions 56 | 57 | const handleEventSelect = (selectedEvent: string) => { 58 | setNewEvent(selectedEvent); 59 | setShowSuggestions(false); 60 | // Optional: Auto subscribe when selecting an event 61 | if (selectedEvent.trim()) { 62 | onSubscribe(selectedEvent.trim()); 63 | setNewEvent(''); 64 | } 65 | }; 66 | 67 | const handleSave = useCallback(() => { 68 | if (message.trim() && (type === 'text' || type === 'binary')) { 69 | onSaveMessage({ 70 | content: message.trim(), 71 | type, 72 | event: protocol === 'socket.io' ? event : undefined 73 | }); 74 | } 75 | }, [message, type, event, protocol, onSaveMessage]); 76 | 77 | const handleSend = useCallback(() => { 78 | if (message.trim() && isConnected) { 79 | onSend(message.trim(), type, protocol === 'socket.io' ? event : undefined); 80 | setMessage(''); 81 | setShowFormatted(false); 82 | } 83 | }, [message, type, event, protocol, isConnected, onSend]); 84 | 85 | // Handle keyboard shortcuts 86 | useEffect(() => { 87 | const handleKeyDown = (e: KeyboardEvent) => { 88 | if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { 89 | e.preventDefault(); 90 | handleSend(); 91 | } 92 | if ((e.metaKey || e.ctrlKey) && e.key === 's') { 93 | e.preventDefault(); 94 | if (message.trim()) { 95 | handleSave(); 96 | } 97 | } 98 | }; 99 | 100 | document.addEventListener('keydown', handleKeyDown); 101 | return () => document.removeEventListener('keydown', handleKeyDown); 102 | }, [handleSend, handleSave, message]); 103 | 104 | const handleFormat = () => { 105 | if (isJSON(message)) { 106 | const formatted = formatJSON(message); 107 | if (formatted) { 108 | setMessage(formatted); 109 | setShowFormatted(true); 110 | } 111 | } 112 | }; 113 | 114 | return ( 115 |
116 |
117 | {protocol === 'socket.io' && ( 118 |
119 | setEvent(e.target.value)} 123 | className="flex-1 px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200" 124 | placeholder="Event name" 125 | disabled={!isConnected} 126 | /> 127 |
128 | { 133 | setNewEvent(e.target.value); 134 | setShowSuggestions(true); 135 | }} 136 | onFocus={() => setShowSuggestions(true)} 137 | className="w-full px-4 py-2 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 dark:bg-gray-700 dark:text-gray-200" 138 | placeholder="Subscribe to event" 139 | disabled={!isConnected} 140 | /> 141 | {showSuggestions && suggestions.length > 0 && ( 142 |
146 | {suggestions.map((suggestion) => ( 147 | 154 | ))} 155 |
156 | )} 157 |
158 | 171 |
172 | )} 173 | 174 | {protocol === 'socket.io' && subscribedEvents.size > 0 && ( 175 |
176 | {Array.from(subscribedEvents).map((e) => ( 177 | 181 | {e} 182 | 188 | 189 | ))} 190 |
191 | )} 192 | 193 |
194 |