@sansavision/create-pulse 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +3 -3
  4. package/templates/aurora-auth-node-demo/README.md +43 -0
  5. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  6. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  7. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  8. package/templates/aurora-auth-node-demo/package.json +39 -0
  9. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  10. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  11. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  12. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  14. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  15. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  16. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  17. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  18. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  19. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  20. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  21. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  24. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  25. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  42. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  43. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  44. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  45. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  46. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  47. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  48. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  49. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  50. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  51. package/templates/nextjs-auth-demo/README.md +1 -1
  52. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  53. package/templates/nextjs-auth-demo/package.json +8 -7
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +124 -23
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +350 -76
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +232 -49
  58. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  59. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  60. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +589 -123
  61. package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +4 -0
  62. package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +53 -5
  63. package/templates/nextjs-auth-demo/src/app/layout.tsx +1 -1
  64. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  65. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  66. package/templates/nextjs-auth-node-demo/README.md +159 -0
  67. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  68. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  69. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  70. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  71. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  72. package/templates/nextjs-auth-node-demo/package.json +38 -0
  73. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  74. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  75. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  76. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  77. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  78. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  79. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  80. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  81. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  82. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  83. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  87. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  88. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  89. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  90. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  91. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  92. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  93. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  94. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  95. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  96. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  97. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  98. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  99. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  100. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  101. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  102. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -3,35 +3,70 @@
3
3
  import { useState, useEffect, useRef, useCallback } from "react";
4
4
  import { useSession } from "@/lib/auth-client";
5
5
  import { connectWithAuth } from "@/lib/pulse";
6
- import { Lock, Send, Shield, Wifi, WifiOff, Loader2, Key, ShieldCheck, Eye, EyeOff } from "lucide-react";
6
+ import { Lock, Send, Shield, Wifi, WifiOff, Loader2, Key, ShieldCheck, Eye, EyeOff, Code, X, ChevronDown, AlertCircle } from "lucide-react";
7
7
  import type { PulseConnection } from "@sansavision/pulse-sdk";
8
8
 
9
9
  interface EncryptedMessage {
10
10
  id: string;
11
11
  sender: string;
12
+ senderId: string;
12
13
  plaintext: string;
13
14
  ciphertext: string;
14
15
  timestamp: number;
16
+ type: "text" | "code";
17
+ language?: string;
15
18
  }
16
19
 
20
+ const CODE_LANGUAGES = [
21
+ { value: "javascript", label: "JavaScript" },
22
+ { value: "typescript", label: "TypeScript" },
23
+ { value: "python", label: "Python" },
24
+ { value: "rust", label: "Rust" },
25
+ { value: "go", label: "Go" },
26
+ { value: "java", label: "Java" },
27
+ { value: "css", label: "CSS" },
28
+ { value: "html", label: "HTML" },
29
+ { value: "sql", label: "SQL" },
30
+ { value: "bash", label: "Bash" },
31
+ { value: "json", label: "JSON" },
32
+ { value: "yaml", label: "YAML" },
33
+ { value: "plaintext", label: "Plain Text" },
34
+ ];
35
+
17
36
  export default function EncryptedChatPage() {
18
37
  const { data: session } = useSession();
19
38
  const [connected, setConnected] = useState(false);
20
39
  const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
21
40
  const [connecting, setConnecting] = useState(true);
41
+
42
+ // Room state
43
+ const [inRoom, setInRoom] = useState(false);
44
+ const [roomId, setRoomId] = useState("");
45
+ const [inputRoomId, setInputRoomId] = useState("");
46
+ const [copied, setCopied] = useState(false);
47
+ const [error, setError] = useState("");
48
+
49
+ // Chat state
22
50
  const [messages, setMessages] = useState<EncryptedMessage[]>([]);
23
51
  const [input, setInput] = useState("");
24
52
  const [showCiphertext, setShowCiphertext] = useState(false);
25
- const [encryptionKey] = useState(() =>
26
- Array.from(crypto.getRandomValues(new Uint8Array(16)))
27
- .map((b) => b.toString(16).padStart(2, "0")).join("")
28
- );
53
+ const [showCodeEditor, setShowCodeEditor] = useState(false);
54
+ const [codeContent, setCodeContent] = useState("");
55
+ const [codeLanguage, setCodeLanguage] = useState("javascript");
56
+ const [showLangDropdown, setShowLangDropdown] = useState(false);
57
+
29
58
  const connRef = useRef<PulseConnection | null>(null);
30
59
  const messagesEndRef = useRef<HTMLDivElement>(null);
60
+ const codeTextareaRef = useRef<HTMLTextAreaElement>(null);
61
+ const streamRef = useRef<ReturnType<PulseConnection["stream"]> | null>(null);
62
+ const sessionRef = useRef(session);
63
+
64
+ // Keep session ref in sync
65
+ useEffect(() => { sessionRef.current = session; }, [session]);
31
66
 
32
67
  const scrollToBottom = useCallback(() => {
33
68
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
34
- }, []);
69
+ }, [messages]);
35
70
 
36
71
  function simpleEncrypt(text: string, key: string): string {
37
72
  return Array.from(text).map((c, i) =>
@@ -46,6 +81,7 @@ export default function EncryptedChatPage() {
46
81
  ).join("");
47
82
  }
48
83
 
84
+ // Connect to relay
49
85
  useEffect(() => {
50
86
  if (!session) return;
51
87
  let cancelled = false;
@@ -54,7 +90,6 @@ export default function EncryptedChatPage() {
54
90
  try {
55
91
  const connection = await connectWithAuth();
56
92
  if (cancelled) return;
57
-
58
93
  connRef.current = connection;
59
94
  setConnected(true);
60
95
  setConnecting(false);
@@ -63,27 +98,6 @@ export default function EncryptedChatPage() {
63
98
  const user = (connection as any).user;
64
99
  if (user) setAuthUser(user);
65
100
 
66
- const stream = connection.stream("encrypted-room");
67
-
68
- stream.on("data", (data: Uint8Array) => {
69
- try {
70
- const msg = JSON.parse(new TextDecoder().decode(data));
71
- if (msg.type === "encrypted-msg") {
72
- const decrypted = simpleDecrypt(msg.ciphertext, encryptionKey);
73
- setMessages((prev) => [
74
- ...prev,
75
- {
76
- id: crypto.randomUUID(),
77
- sender: msg.sender,
78
- plaintext: decrypted,
79
- ciphertext: msg.ciphertext,
80
- timestamp: Date.now(),
81
- },
82
- ]);
83
- }
84
- } catch { /* ignore */ }
85
- });
86
-
87
101
  connection.on("disconnect", () => setConnected(false));
88
102
  connection.on("reconnected", () => setConnected(true));
89
103
  } catch (err) {
@@ -91,29 +105,169 @@ export default function EncryptedChatPage() {
91
105
  setConnecting(false);
92
106
  }
93
107
  }
94
-
95
108
  init();
96
109
  return () => {
97
110
  cancelled = true;
98
111
  connRef.current?.disconnect();
99
112
  };
100
- // eslint-disable-next-line react-hooks/exhaustive-deps
101
113
  }, [session]);
102
114
 
115
+ // Room Stream logic — set up ONCE when entering a room
116
+ useEffect(() => {
117
+ if (!inRoom || !roomId || !connRef.current) return;
118
+
119
+ // Load messages from localStorage for this room
120
+ try {
121
+ const stored = localStorage.getItem(`pulse-encrypted-${roomId}`);
122
+ if (stored) {
123
+ setMessages(JSON.parse(stored));
124
+ } else {
125
+ setMessages([]);
126
+ }
127
+ } catch { }
128
+
129
+ const streamName = `encrypted-room-${roomId}`;
130
+ const stream = connRef.current.stream(streamName);
131
+ streamRef.current = stream;
132
+
133
+ const handleData = (data: Uint8Array) => {
134
+ try {
135
+ const msg = JSON.parse(new TextDecoder().decode(data));
136
+ const sess = sessionRef.current;
137
+ const selfId = sess?.user?.id || "unknown";
138
+
139
+ // Use userId for filtering — NOT name (two users could have the same name)
140
+ if ((msg.type === "encrypted-msg" || msg.type === "encrypted-code") && msg.senderId !== selfId) {
141
+ const decrypted = simpleDecrypt(msg.ciphertext, roomId);
142
+ setMessages((prev) => [
143
+ ...prev,
144
+ {
145
+ id: crypto.randomUUID(),
146
+ sender: msg.sender,
147
+ senderId: msg.senderId,
148
+ plaintext: decrypted,
149
+ ciphertext: msg.ciphertext,
150
+ timestamp: Date.now(),
151
+ type: msg.type === "encrypted-code" ? "code" : "text",
152
+ language: msg.language,
153
+ },
154
+ ]);
155
+ }
156
+ } catch { /* ignore */ }
157
+ };
158
+
159
+ stream.on("data", handleData);
160
+
161
+ // Cleanup: drop our reference so the stream can be GC'd
162
+ return () => {
163
+ streamRef.current = null;
164
+ };
165
+ // Only depend on inRoom and roomId — NOT session (avoids re-creating stream)
166
+ }, [inRoom, roomId]);
167
+
168
+ // Save to localStorage whenever messages change in a room
169
+ useEffect(() => {
170
+ if (inRoom && roomId) {
171
+ localStorage.setItem(`pulse-encrypted-${roomId}`, JSON.stringify(messages.slice(-100)));
172
+ }
173
+ }, [messages, inRoom, roomId]);
174
+
103
175
  useEffect(scrollToBottom, [messages, scrollToBottom]);
104
176
 
177
+ function createRoom() {
178
+ // Generate a 32-hex string to act as both Room ID and Encryption Key
179
+ const newKey = Array.from(crypto.getRandomValues(new Uint8Array(16)))
180
+ .map((b) => b.toString(16).padStart(2, "0")).join("");
181
+ setRoomId(newKey);
182
+ setInRoom(true);
183
+ }
184
+
185
+ function joinRoom(e: React.FormEvent) {
186
+ e.preventDefault();
187
+ const cleaned = inputRoomId.trim();
188
+ if (!cleaned) return;
189
+
190
+ if (!/^[a-fA-F0-9]{32}$/.test(cleaned)) {
191
+ setError("Invalid key format. Must be a 32-character hex string.");
192
+ return;
193
+ }
194
+
195
+ setError("");
196
+ setRoomId(cleaned);
197
+ setInRoom(true);
198
+ }
199
+
200
+ function copyRoomId() {
201
+ navigator.clipboard.writeText(roomId);
202
+ setCopied(true);
203
+ setTimeout(() => setCopied(false), 2000);
204
+ }
205
+
105
206
  function sendMessage(e: React.FormEvent) {
106
207
  e.preventDefault();
107
- if (!input.trim() || !connRef.current) return;
208
+ if (!input.trim() || !streamRef.current || !inRoom) return;
108
209
 
109
210
  const plaintext = input.trim();
110
- const ciphertext = simpleEncrypt(plaintext, encryptionKey);
211
+ const ciphertext = simpleEncrypt(plaintext, roomId);
212
+ const sender = session?.user?.name || "Anonymous";
213
+ const senderId = session?.user?.id || "unknown";
214
+
215
+ streamRef.current.send(JSON.stringify({ type: "encrypted-msg", sender, senderId, ciphertext }));
111
216
 
112
- const stream = connRef.current.stream("encrypted-room");
113
- stream.send(JSON.stringify({ type: "encrypted-msg", sender: session?.user?.name || "Anonymous", ciphertext }));
217
+ setMessages((prev) => [
218
+ ...prev,
219
+ { id: crypto.randomUUID(), sender, senderId, plaintext, ciphertext, timestamp: Date.now(), type: "text" },
220
+ ]);
114
221
  setInput("");
115
222
  }
116
223
 
224
+ function sendCode() {
225
+ if (!codeContent.trim() || !streamRef.current || !inRoom) return;
226
+
227
+ const plaintext = codeContent.trim();
228
+ const ciphertext = simpleEncrypt(plaintext, roomId);
229
+ const sender = session?.user?.name || "Anonymous";
230
+ const senderId = session?.user?.id || "unknown";
231
+
232
+ streamRef.current.send(JSON.stringify({
233
+ type: "encrypted-code",
234
+ sender,
235
+ senderId,
236
+ ciphertext,
237
+ language: codeLanguage,
238
+ }));
239
+
240
+ setMessages((prev) => [
241
+ ...prev,
242
+ { id: crypto.randomUUID(), sender, senderId, plaintext, ciphertext, timestamp: Date.now(), type: "code", language: codeLanguage },
243
+ ]);
244
+ setCodeContent("");
245
+ setShowCodeEditor(false);
246
+ }
247
+
248
+ function handleCodeKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
249
+ if (e.key === "Tab") {
250
+ e.preventDefault();
251
+ const textarea = e.currentTarget;
252
+ const start = textarea.selectionStart;
253
+ const end = textarea.selectionEnd;
254
+ const newValue = codeContent.substring(0, start) + " " + codeContent.substring(end);
255
+ setCodeContent(newValue);
256
+ setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + 2; }, 0);
257
+ }
258
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
259
+ e.preventDefault();
260
+ sendCode();
261
+ }
262
+ }
263
+
264
+ function openCodeEditor() {
265
+ setShowCodeEditor(true);
266
+ setTimeout(() => codeTextareaRef.current?.focus(), 100);
267
+ }
268
+
269
+ const getLanguageLabel = (val: string) => CODE_LANGUAGES.find((l) => l.value === val)?.label || val;
270
+
117
271
  return (
118
272
  <div className="h-screen flex flex-col">
119
273
  <div className="p-4 border-b border-slate-800">
@@ -124,7 +278,7 @@ export default function EncryptedChatPage() {
124
278
  </div>
125
279
  <div>
126
280
  <h1 className="text-lg font-semibold">E2E Encrypted Chat</h1>
127
- <p className="text-xs text-slate-500">Messages encrypted before leaving your browser</p>
281
+ <p className="text-xs text-slate-500">Secure end-to-end messaging</p>
128
282
  </div>
129
283
  </div>
130
284
  <div className="flex items-center gap-3">
@@ -142,57 +296,177 @@ export default function EncryptedChatPage() {
142
296
  </div>
143
297
  </div>
144
298
 
145
- <div className="flex items-center gap-4">
146
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-purple-500/10 border border-purple-500/20">
147
- <Key className="w-3.5 h-3.5 text-purple-400" />
148
- <code className="text-xs text-purple-400 font-mono">{encryptionKey.slice(0, 16)}...</code>
149
- </div>
150
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-violet-500/10 border border-violet-500/20">
151
- <ShieldCheck className="w-3.5 h-3.5 text-violet-400" />
152
- <span className="text-xs text-violet-400">XOR cipher (demo)</span>
299
+ {inRoom && (
300
+ <div className="flex items-center gap-4">
301
+ <div
302
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-purple-500/10 border border-purple-500/20 cursor-pointer hover:bg-purple-500/20"
303
+ onClick={copyRoomId}
304
+ title="Click to copy room key"
305
+ >
306
+ <Key className="w-3.5 h-3.5 text-purple-400" />
307
+ <code className="text-xs text-purple-400 font-mono">
308
+ {copied ? "Copied!" : roomId.slice(0, 16) + "..."}
309
+ </code>
310
+ </div>
311
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-violet-500/10 border border-violet-500/20">
312
+ <ShieldCheck className="w-3.5 h-3.5 text-violet-400" />
313
+ <span className="text-xs text-violet-400">XOR cipher (demo)</span>
314
+ </div>
315
+ <button onClick={() => setShowCiphertext(!showCiphertext)}
316
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 hover:border-slate-600 transition-colors">
317
+ {showCiphertext ? <EyeOff className="w-3.5 h-3.5 text-slate-400" /> : <Eye className="w-3.5 h-3.5 text-slate-400" />}
318
+ <span className="text-xs text-slate-400">{showCiphertext ? "Hide" : "Show"} ciphertext</span>
319
+ </button>
320
+ <button onClick={() => { setInRoom(false); setMessages([]); setRoomId(""); streamRef.current = null; }}
321
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 hover:border-slate-600 transition-colors ml-auto text-xs text-slate-400">
322
+ Leave Room
323
+ </button>
153
324
  </div>
154
- <button onClick={() => setShowCiphertext(!showCiphertext)}
155
- className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 hover:border-slate-600 transition-colors">
156
- {showCiphertext ? <EyeOff className="w-3.5 h-3.5 text-slate-400" /> : <Eye className="w-3.5 h-3.5 text-slate-400" />}
157
- <span className="text-xs text-slate-400">{showCiphertext ? "Hide" : "Show"} ciphertext</span>
158
- </button>
159
- </div>
325
+ )}
160
326
  </div>
161
327
 
162
- <div className="flex-1 overflow-y-auto p-4 space-y-3">
163
- {connecting && (
164
- <div className="flex items-center justify-center py-20">
165
- <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" /><span className="text-slate-400">Establishing encrypted channel...</span>
166
- </div>
167
- )}
328
+ {connecting ? (
329
+ <div className="flex-1 flex items-center justify-center border-b border-t border-slate-800">
330
+ <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" />
331
+ <span className="text-slate-400">Connecting to relay...</span>
332
+ </div>
333
+ ) : !inRoom ? (
334
+ /* Room selection */
335
+ <div className="flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center">
336
+ <div className="max-w-xl w-full mx-auto space-y-6">
337
+ <div className="glass rounded-2xl p-8 text-center border border-slate-800 shadow-xl">
338
+ <Lock className="w-16 h-16 text-purple-400 mx-auto mb-4" />
339
+ <h2 className="text-xl font-bold mb-2">Join Secure Room</h2>
340
+ <p className="text-sm text-slate-400 mb-8">
341
+ Create a new secure room to generate an encryption key, or join an existing room by pasting its key.
342
+ </p>
343
+
344
+ <button
345
+ onClick={createRoom}
346
+ disabled={!connected}
347
+ className="w-full px-6 py-3 bg-gradient-to-r from-purple-500 to-violet-500 hover:from-purple-400 hover:to-violet-400 rounded-xl font-semibold text-lg transition-all disabled:opacity-50 mb-6 flex items-center justify-center gap-2"
348
+ >
349
+ <Lock className="w-5 h-5" /> Create Secure Room
350
+ </button>
168
351
 
169
- {messages.map((msg) => (
170
- <div key={msg.id}>
171
- <div className={`flex ${msg.sender === session?.user?.name ? "justify-end" : "justify-start"}`}>
172
- <div className={`max-w-md px-4 py-2.5 rounded-2xl text-sm ${msg.sender === session?.user?.name ? "bg-purple-600 text-white rounded-br-md" : "glass text-slate-200 rounded-bl-md"}`}>
173
- {msg.sender !== session?.user?.name && <div className="text-xs text-purple-400 font-medium mb-1">{msg.sender}</div>}
174
- <div className="flex items-center gap-1.5"><Lock className="w-3 h-3 text-green-400 shrink-0" />{msg.plaintext}</div>
352
+ <div className="relative mb-6">
353
+ <div className="absolute inset-0 flex items-center"><div className="w-full border-t border-slate-700" /></div>
354
+ <div className="relative flex justify-center text-xs uppercase"><span className="bg-slate-900 px-3 text-slate-500">or join existing</span></div>
175
355
  </div>
356
+
357
+ {error && (
358
+ <div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center gap-2 text-red-400 text-sm">
359
+ <AlertCircle className="w-4 h-4 shrink-0" />
360
+ <p>{error}</p>
361
+ </div>
362
+ )}
363
+
364
+ <form onSubmit={joinRoom} className="flex gap-3">
365
+ <input type="text" value={inputRoomId} onChange={(e) => { setInputRoomId(e.target.value); setError(""); }} placeholder="Enter 32-char hex key..."
366
+ className="flex-1 px-4 py-3 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600" />
367
+ <button type="submit" disabled={!connected || !inputRoomId.trim()}
368
+ className="px-6 py-2.5 bg-purple-600 hover:bg-purple-500 rounded-xl font-medium transition-all disabled:opacity-50">
369
+ Join
370
+ </button>
371
+ </form>
176
372
  </div>
177
- {showCiphertext && (
178
- <div className={`mt-1 text-xs font-mono text-slate-600 ${msg.sender === session?.user?.name ? "text-right" : "text-left"} px-4`}>
179
- 🔒 {msg.ciphertext.slice(0, 40)}...
373
+ </div>
374
+ </div>
375
+ ) : (
376
+ /* In Room Chat */
377
+ <>
378
+ <div className="flex-1 overflow-y-auto p-4 space-y-3">
379
+ {messages.length === 0 && (
380
+ <div className="flex flex-col items-center justify-center h-full text-center text-slate-500">
381
+ <Lock className="w-8 h-8 mb-2 opacity-50" />
382
+ <p>End-to-End Encrypted Session Active</p>
383
+ <p className="text-xs opacity-75 mt-1">Share the room key for others to join.</p>
180
384
  </div>
181
385
  )}
386
+ {messages.map((msg) => (
387
+ <div key={msg.id}>
388
+ <div className={`flex ${msg.senderId === session?.user?.id ? "justify-end" : "justify-start"}`}>
389
+ {msg.type === "code" ? (
390
+ <div className={`max-w-lg w-full rounded-2xl overflow-hidden border ${msg.senderId === session?.user?.id ? "border-purple-500/30 bg-purple-950/30" : "border-slate-700 bg-slate-800/50"}`}>
391
+ <div className="flex items-center justify-between px-4 py-2 border-b border-slate-700/50 bg-slate-900/50">
392
+ <div className="flex items-center gap-2">
393
+ <Code className="w-3.5 h-3.5 text-purple-400" />
394
+ <span className="text-xs font-medium text-purple-400">{getLanguageLabel(msg.language || "plaintext")}</span>
395
+ </div>
396
+ <div className="flex items-center gap-2">
397
+ <Lock className="w-3 h-3 text-green-400" />
398
+ {msg.senderId !== session?.user?.id && <span className="text-xs text-slate-400">{msg.sender}</span>}
399
+ </div>
400
+ </div>
401
+ <pre className="p-4 text-sm font-mono overflow-x-auto text-slate-200 leading-relaxed"><code>{msg.plaintext}</code></pre>
402
+ </div>
403
+ ) : (
404
+ <div className={`max-w-md px-4 py-2.5 rounded-2xl text-sm ${msg.senderId === session?.user?.id ? "bg-purple-600 text-white rounded-br-md" : "glass text-slate-200 rounded-bl-md"}`}>
405
+ {msg.senderId !== session?.user?.id && <div className="text-xs text-purple-400 font-medium mb-1">{msg.sender}</div>}
406
+ <div className="flex items-center gap-1.5"><Lock className="w-3 h-3 text-green-400 shrink-0" />{msg.plaintext}</div>
407
+ </div>
408
+ )}
409
+ </div>
410
+ {showCiphertext && (
411
+ <div className={`mt-1 text-xs font-mono text-slate-600 ${msg.senderId === session?.user?.id ? "text-right" : "text-left"} px-4`}>
412
+ 🔒 {msg.ciphertext.slice(0, 40)}...
413
+ </div>
414
+ )}
415
+ </div>
416
+ ))}
417
+ <div ref={messagesEndRef} />
182
418
  </div>
183
- ))}
184
- <div ref={messagesEndRef} />
185
- </div>
186
419
 
187
- <form onSubmit={sendMessage} className="p-4 border-t border-slate-800 flex gap-3">
188
- <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type an encrypted message..."
189
- disabled={!connected}
190
- className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600 disabled:opacity-50" />
191
- <button type="submit" disabled={!connected || !input.trim()}
192
- className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed">
193
- <Send className="w-4 h-4" />
194
- </button>
195
- </form>
420
+ {showCodeEditor && (
421
+ <div className="border-t border-slate-800 bg-slate-900/80 backdrop-blur">
422
+ <div className="flex items-center justify-between px-4 py-2 border-b border-slate-800">
423
+ <div className="flex items-center gap-3">
424
+ <div className="flex items-center gap-2">
425
+ <Code className="w-4 h-4 text-purple-400" />
426
+ <span className="text-sm font-medium text-purple-400">Share Code Snippet</span>
427
+ </div>
428
+ <div className="relative">
429
+ <button onClick={() => setShowLangDropdown(!showLangDropdown)} className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-slate-800 border border-slate-700 hover:border-slate-600 transition-colors text-xs">
430
+ {getLanguageLabel(codeLanguage)}<ChevronDown className="w-3 h-3 text-slate-400" />
431
+ </button>
432
+ {showLangDropdown && (
433
+ <div className="absolute bottom-full left-0 mb-1 w-44 max-h-52 overflow-y-auto rounded-lg bg-slate-800 border border-slate-700 shadow-xl z-50">
434
+ {CODE_LANGUAGES.map((lang) => (
435
+ <button key={lang.value} onClick={() => { setCodeLanguage(lang.value); setShowLangDropdown(false); }}
436
+ className={`w-full text-left px-3 py-2 text-xs hover:bg-slate-700 transition-colors ${codeLanguage === lang.value ? "text-purple-400 bg-purple-500/10" : "text-slate-300"}`}>
437
+ {lang.label}
438
+ </button>
439
+ ))}
440
+ </div>
441
+ )}
442
+ </div>
443
+ </div>
444
+ <div className="flex items-center gap-2">
445
+ <span className="text-[10px] text-slate-500">⌘+Enter to send</span>
446
+ <button onClick={() => { setShowCodeEditor(false); setCodeContent(""); }} className="p-1 rounded hover:bg-slate-800 transition-colors"><X className="w-4 h-4 text-slate-400" /></button>
447
+ </div>
448
+ </div>
449
+ <div className="p-3">
450
+ <textarea ref={codeTextareaRef} value={codeContent} onChange={(e) => setCodeContent(e.target.value)} onKeyDown={handleCodeKeyDown} placeholder="Paste or type your code here..." spellCheck={false}
451
+ className="w-full h-32 px-4 py-3 rounded-xl bg-slate-950 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm font-mono text-slate-200 resize-none transition-colors placeholder:text-slate-600 leading-relaxed" />
452
+ <div className="flex justify-end gap-2 mt-2">
453
+ <button onClick={() => { setShowCodeEditor(false); setCodeContent(""); }} className="px-4 py-2 rounded-lg text-xs text-slate-400 hover:bg-slate-800 transition-colors">Cancel</button>
454
+ <button onClick={sendCode} disabled={!connected || !codeContent.trim()} className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-xs font-semibold transition-all disabled:opacity-50 flex items-center gap-1.5"><Lock className="w-3 h-3" />Send Encrypted</button>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ )}
459
+
460
+ <form onSubmit={sendMessage} className="p-4 border-t border-slate-800 flex gap-3">
461
+ <button type="button" onClick={openCodeEditor} disabled={!connected} className="px-3 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 hover:border-purple-500 hover:bg-purple-500/10 transition-all disabled:opacity-50 group" title="Share code snippet">
462
+ <Code className="w-4 h-4 text-slate-400 group-hover:text-purple-400 transition-colors" />
463
+ </button>
464
+ <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type an encrypted message..." disabled={!connected}
465
+ className="flex-1 px-4 py-2.5 rounded-xl bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600 disabled:opacity-50" />
466
+ <button type="submit" disabled={!connected || !input.trim()} className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 rounded-xl transition-all disabled:opacity-50"><Send className="w-4 h-4" /></button>
467
+ </form>
468
+ </>
469
+ )}
196
470
  </div>
197
471
  );
198
472
  }