/ concept-collection / trystero-messaging-demo
Sign in
concept-collection / trystero-messaging-demo
trystero-messaging-demo / src / components / Composer.tsx
138 lines · 3.7 KBBlameHistoryRaw
1import {useRef, useState} from 'react'
2import {formatBytes, randomBytes} from '../util'
4type Props = {
5 target: string | null
6 targetName: string
7 onSendText: (text: string, target: string | null) => void
8 onSendBinary: (
9 data: Uint8Array<ArrayBuffer>,
10 fileName: string,
11 mime: string,
12 target: string | null
13 ) => Promise<void>
16const RANDOM_SIZES = [
17 {label: '256 KB', bytes: 256 * 1024},
18 {label: '1 MB', bytes: 1024 * 1024},
19 {label: '8 MB', bytes: 8 * 1024 * 1024},
20 {label: '32 MB', bytes: 32 * 1024 * 1024}
23export function Composer({target, targetName, onSendText, onSendBinary}: Props) {
24 const [text, setText] = useState('')
25 const [file, setFile] = useState<File | null>(null)
26 const [randomSize, setRandomSize] = useState(RANDOM_SIZES[1].bytes)
27 const [sending, setSending] = useState(false)
28 const fileInputRef = useRef<HTMLInputElement>(null)
30 const sendText = () => {
31 const trimmed = text.trim()
32 if (!trimmed) return
33 onSendText(trimmed, target)
34 setText('')
35 }
37 const sendFile = async () => {
38 if (!file || sending) return
39 setSending(true)
40 try {
41 const buf = new Uint8Array(await file.arrayBuffer())
42 await onSendBinary(
43 buf,
44 file.name,
45 file.type || 'application/octet-stream',
46 target
47 )
48 } finally {
49 setSending(false)
50 }
51 }
53 const sendRandom = async () => {
54 if (sending) return
55 setSending(true)
56 try {
57 const bytes = randomBytes(randomSize)
58 const name = `random-${formatBytes(randomSize).replace(' ', '')}.bin`
59 await onSendBinary(bytes, name, 'application/octet-stream', target)
60 } finally {
61 setSending(false)
62 }
63 }
65 return (
66 <div className="panel">
67 <h2 className="panel-title">
68 Compose <span className="to-badge">→ {targetName}</span>
69 </h2>
71 <div className="composer-row">
72 <input
73 className="text-input"
74 placeholder={`Message ${targetName}…`}
75 value={text}
76 onChange={e => setText(e.target.value)}
77 onKeyDown={e => {
78 if (e.key === 'Enter') sendText()
79 }}
80 />
81 <button className="btn primary" onClick={sendText} disabled={!text.trim()}>
82 Send text
83 </button>
84 </div>
86 <div className="divider">
87 <span>binary payload</span>
88 </div>
90 <div className="composer-row">
91 <input
92 ref={fileInputRef}
93 type="file"
94 className="file-input"
95 onChange={e => setFile(e.target.files?.[0] ?? null)}
96 />
97 <button
98 className="btn"
99 onClick={sendFile}
100 disabled={!file || sending}
101 >
102 {sending ? 'Sending…' : 'Send file'}
103 </button>
104 </div>
105 {file && (
106 <p className="hint">
107 Selected: <b>{file.name}</b> ({formatBytes(file.size)})
108 </p>
109 )}
111 <div className="composer-row">
112 <label className="hint" style={{margin: 0}}>
113 …or generate a random payload:
114 </label>
115 <select
116 className="select"
117 value={randomSize}
118 onChange={e => setRandomSize(Number(e.target.value))}
119 >
120 {RANDOM_SIZES.map(s => (
121 <option key={s.bytes} value={s.bytes}>
122 {s.label}
123 </option>
124 ))}
125 </select>
126 <button className="btn" onClick={sendRandom} disabled={sending}>
127 {sending ? 'Sending…' : 'Send random'}
128 </button>
129 </div>
131 <p className="hint subtle">
132 Large payloads are automatically chunked & throttled by Trystero and sent
133 directly peer-to-peer (end-to-end encrypted). A SHA-256 is computed on
134 both ends so you can confirm the bytes arrived intact.
135 </p>
136 </div>
137 )
moveopenescclose