@sansavision/create-pulse 0.4.4 → 0.4.6

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 (97) hide show
  1. package/dist/index.js +2 -0
  2. package/package.json +2 -2
  3. package/templates/aurora-auth-node-demo/README.md +43 -0
  4. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  5. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  6. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  7. package/templates/aurora-auth-node-demo/package.json +39 -0
  8. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  9. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  10. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  11. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  12. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  14. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  15. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  16. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  17. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  18. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  19. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  20. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  21. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  24. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  25. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  42. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  43. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  44. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  45. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  46. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  47. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  48. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  49. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  50. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  51. package/templates/nextjs-auth-demo/package.json +8 -7
  52. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
  53. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
  58. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
  59. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  60. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  61. package/templates/nextjs-auth-node-demo/README.md +159 -0
  62. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  63. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  64. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  65. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  66. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  67. package/templates/nextjs-auth-node-demo/package.json +38 -0
  68. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  69. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  70. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  71. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  72. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  73. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  74. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  75. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  76. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  77. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  78. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  79. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  80. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  81. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  82. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  83. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  87. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  88. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  89. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  90. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  91. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  92. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  93. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  94. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  95. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  96. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  97. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -3,12 +3,13 @@
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, Code, X, ChevronDown } 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;
@@ -37,6 +38,15 @@ export default function EncryptedChatPage() {
37
38
  const [connected, setConnected] = useState(false);
38
39
  const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
39
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
40
50
  const [messages, setMessages] = useState<EncryptedMessage[]>([]);
41
51
  const [input, setInput] = useState("");
42
52
  const [showCiphertext, setShowCiphertext] = useState(false);
@@ -44,17 +54,19 @@ export default function EncryptedChatPage() {
44
54
  const [codeContent, setCodeContent] = useState("");
45
55
  const [codeLanguage, setCodeLanguage] = useState("javascript");
46
56
  const [showLangDropdown, setShowLangDropdown] = useState(false);
47
- const [encryptionKey] = useState(() =>
48
- Array.from(crypto.getRandomValues(new Uint8Array(16)))
49
- .map((b) => b.toString(16).padStart(2, "0")).join("")
50
- );
57
+
51
58
  const connRef = useRef<PulseConnection | null>(null);
52
59
  const messagesEndRef = useRef<HTMLDivElement>(null);
53
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]);
54
66
 
55
67
  const scrollToBottom = useCallback(() => {
56
68
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
57
- }, []);
69
+ }, [messages]);
58
70
 
59
71
  function simpleEncrypt(text: string, key: string): string {
60
72
  return Array.from(text).map((c, i) =>
@@ -69,6 +81,7 @@ export default function EncryptedChatPage() {
69
81
  ).join("");
70
82
  }
71
83
 
84
+ // Connect to relay
72
85
  useEffect(() => {
73
86
  if (!session) return;
74
87
  let cancelled = false;
@@ -77,7 +90,6 @@ export default function EncryptedChatPage() {
77
90
  try {
78
91
  const connection = await connectWithAuth();
79
92
  if (cancelled) return;
80
-
81
93
  connRef.current = connection;
82
94
  setConnected(true);
83
95
  setConnecting(false);
@@ -86,31 +98,6 @@ export default function EncryptedChatPage() {
86
98
  const user = (connection as any).user;
87
99
  if (user) setAuthUser(user);
88
100
 
89
- const stream = connection.stream("encrypted-room");
90
-
91
- // Listen for messages (skip self-echoes — we add our own messages locally)
92
- stream.on("data", (data: Uint8Array) => {
93
- try {
94
- const msg = JSON.parse(new TextDecoder().decode(data));
95
- const selfName = session?.user?.name || "Anonymous";
96
- if ((msg.type === "encrypted-msg" || msg.type === "encrypted-code") && msg.sender !== selfName) {
97
- const decrypted = simpleDecrypt(msg.ciphertext, encryptionKey);
98
- setMessages((prev) => [
99
- ...prev,
100
- {
101
- id: crypto.randomUUID(),
102
- sender: msg.sender,
103
- plaintext: decrypted,
104
- ciphertext: msg.ciphertext,
105
- timestamp: Date.now(),
106
- type: msg.type === "encrypted-code" ? "code" : "text",
107
- language: msg.language,
108
- },
109
- ]);
110
- }
111
- } catch { /* ignore */ }
112
- });
113
-
114
101
  connection.on("disconnect", () => setConnected(false));
115
102
  connection.on("reconnected", () => setConnected(true));
116
103
  } catch (err) {
@@ -118,77 +105,147 @@ export default function EncryptedChatPage() {
118
105
  setConnecting(false);
119
106
  }
120
107
  }
121
-
122
108
  init();
123
109
  return () => {
124
110
  cancelled = true;
125
111
  connRef.current?.disconnect();
126
112
  };
127
- // eslint-disable-next-line react-hooks/exhaustive-deps
128
113
  }, [session]);
129
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
+
130
175
  useEffect(scrollToBottom, [messages, scrollToBottom]);
131
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
+
132
206
  function sendMessage(e: React.FormEvent) {
133
207
  e.preventDefault();
134
- if (!input.trim() || !connRef.current) return;
208
+ if (!input.trim() || !streamRef.current || !inRoom) return;
135
209
 
136
210
  const plaintext = input.trim();
137
- const ciphertext = simpleEncrypt(plaintext, encryptionKey);
211
+ const ciphertext = simpleEncrypt(plaintext, roomId);
138
212
  const sender = session?.user?.name || "Anonymous";
213
+ const senderId = session?.user?.id || "unknown";
139
214
 
140
- const stream = connRef.current.stream("encrypted-room");
141
- stream.send(JSON.stringify({ type: "encrypted-msg", sender, ciphertext }));
215
+ streamRef.current.send(JSON.stringify({ type: "encrypted-msg", sender, senderId, ciphertext }));
142
216
 
143
- // Add to local state immediately (relay echoes, but we filter self in on("data"))
144
217
  setMessages((prev) => [
145
218
  ...prev,
146
- {
147
- id: crypto.randomUUID(),
148
- sender,
149
- plaintext,
150
- ciphertext,
151
- timestamp: Date.now(),
152
- type: "text",
153
- },
219
+ { id: crypto.randomUUID(), sender, senderId, plaintext, ciphertext, timestamp: Date.now(), type: "text" },
154
220
  ]);
155
221
  setInput("");
156
222
  }
157
223
 
158
224
  function sendCode() {
159
- if (!codeContent.trim() || !connRef.current) return;
225
+ if (!codeContent.trim() || !streamRef.current || !inRoom) return;
160
226
 
161
227
  const plaintext = codeContent.trim();
162
- const ciphertext = simpleEncrypt(plaintext, encryptionKey);
228
+ const ciphertext = simpleEncrypt(plaintext, roomId);
163
229
  const sender = session?.user?.name || "Anonymous";
230
+ const senderId = session?.user?.id || "unknown";
164
231
 
165
- const stream = connRef.current.stream("encrypted-room");
166
- stream.send(JSON.stringify({
232
+ streamRef.current.send(JSON.stringify({
167
233
  type: "encrypted-code",
168
234
  sender,
235
+ senderId,
169
236
  ciphertext,
170
237
  language: codeLanguage,
171
238
  }));
172
239
 
173
- // Add to local state immediately
174
240
  setMessages((prev) => [
175
241
  ...prev,
176
- {
177
- id: crypto.randomUUID(),
178
- sender,
179
- plaintext,
180
- ciphertext,
181
- timestamp: Date.now(),
182
- type: "code",
183
- language: codeLanguage,
184
- },
242
+ { id: crypto.randomUUID(), sender, senderId, plaintext, ciphertext, timestamp: Date.now(), type: "code", language: codeLanguage },
185
243
  ]);
186
244
  setCodeContent("");
187
245
  setShowCodeEditor(false);
188
246
  }
189
247
 
190
248
  function handleCodeKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
191
- // Handle Tab key for indentation
192
249
  if (e.key === "Tab") {
193
250
  e.preventDefault();
194
251
  const textarea = e.currentTarget;
@@ -196,12 +253,8 @@ export default function EncryptedChatPage() {
196
253
  const end = textarea.selectionEnd;
197
254
  const newValue = codeContent.substring(0, start) + " " + codeContent.substring(end);
198
255
  setCodeContent(newValue);
199
- // Set cursor position after the tab
200
- setTimeout(() => {
201
- textarea.selectionStart = textarea.selectionEnd = start + 2;
202
- }, 0);
256
+ setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + 2; }, 0);
203
257
  }
204
- // Cmd/Ctrl+Enter to send
205
258
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
206
259
  e.preventDefault();
207
260
  sendCode();
@@ -213,8 +266,7 @@ export default function EncryptedChatPage() {
213
266
  setTimeout(() => codeTextareaRef.current?.focus(), 100);
214
267
  }
215
268
 
216
- const getLanguageLabel = (val: string) =>
217
- CODE_LANGUAGES.find((l) => l.value === val)?.label || val;
269
+ const getLanguageLabel = (val: string) => CODE_LANGUAGES.find((l) => l.value === val)?.label || val;
218
270
 
219
271
  return (
220
272
  <div className="h-screen flex flex-col">
@@ -226,7 +278,7 @@ export default function EncryptedChatPage() {
226
278
  </div>
227
279
  <div>
228
280
  <h1 className="text-lg font-semibold">E2E Encrypted Chat</h1>
229
- <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>
230
282
  </div>
231
283
  </div>
232
284
  <div className="flex items-center gap-3">
@@ -244,168 +296,177 @@ export default function EncryptedChatPage() {
244
296
  </div>
245
297
  </div>
246
298
 
247
- <div className="flex items-center gap-4">
248
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-purple-500/10 border border-purple-500/20">
249
- <Key className="w-3.5 h-3.5 text-purple-400" />
250
- <code className="text-xs text-purple-400 font-mono">{encryptionKey.slice(0, 16)}...</code>
251
- </div>
252
- <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-violet-500/10 border border-violet-500/20">
253
- <ShieldCheck className="w-3.5 h-3.5 text-violet-400" />
254
- <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>
255
324
  </div>
256
- <button onClick={() => setShowCiphertext(!showCiphertext)}
257
- 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">
258
- {showCiphertext ? <EyeOff className="w-3.5 h-3.5 text-slate-400" /> : <Eye className="w-3.5 h-3.5 text-slate-400" />}
259
- <span className="text-xs text-slate-400">{showCiphertext ? "Hide" : "Show"} ciphertext</span>
260
- </button>
261
- </div>
325
+ )}
262
326
  </div>
263
327
 
264
- <div className="flex-1 overflow-y-auto p-4 space-y-3">
265
- {connecting && (
266
- <div className="flex items-center justify-center py-20">
267
- <Loader2 className="w-6 h-6 animate-spin text-purple-500 mr-2" /><span className="text-slate-400">Establishing encrypted channel...</span>
268
- </div>
269
- )}
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>
270
343
 
271
- {messages.map((msg) => (
272
- <div key={msg.id}>
273
- <div className={`flex ${msg.sender === session?.user?.name ? "justify-end" : "justify-start"}`}>
274
- {msg.type === "code" ? (
275
- /* Code snippet message */
276
- <div className={`max-w-lg w-full rounded-2xl overflow-hidden border ${msg.sender === session?.user?.name
277
- ? "border-purple-500/30 bg-purple-950/30"
278
- : "border-slate-700 bg-slate-800/50"
279
- }`}>
280
- <div className="flex items-center justify-between px-4 py-2 border-b border-slate-700/50 bg-slate-900/50">
281
- <div className="flex items-center gap-2">
282
- <Code className="w-3.5 h-3.5 text-purple-400" />
283
- <span className="text-xs font-medium text-purple-400">
284
- {getLanguageLabel(msg.language || "plaintext")}
285
- </span>
286
- </div>
287
- <div className="flex items-center gap-2">
288
- <Lock className="w-3 h-3 text-green-400" />
289
- {msg.sender !== session?.user?.name && (
290
- <span className="text-xs text-slate-400">{msg.sender}</span>
291
- )}
292
- </div>
293
- </div>
294
- <pre className="p-4 text-sm font-mono overflow-x-auto text-slate-200 leading-relaxed">
295
- <code>{msg.plaintext}</code>
296
- </pre>
297
- </div>
298
- ) : (
299
- /* Normal text message */
300
- <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"}`}>
301
- {msg.sender !== session?.user?.name && <div className="text-xs text-purple-400 font-medium mb-1">{msg.sender}</div>}
302
- <div className="flex items-center gap-1.5"><Lock className="w-3 h-3 text-green-400 shrink-0" />{msg.plaintext}</div>
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>
351
+
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>
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>
303
361
  </div>
304
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>
305
372
  </div>
306
- {showCiphertext && (
307
- <div className={`mt-1 text-xs font-mono text-slate-600 ${msg.sender === session?.user?.name ? "text-right" : "text-left"} px-4`}>
308
- 🔒 {msg.ciphertext.slice(0, 40)}...
309
- </div>
310
- )}
311
373
  </div>
312
- ))}
313
- <div ref={messagesEndRef} />
314
- </div>
315
-
316
- {/* Code Editor Panel */}
317
- {showCodeEditor && (
318
- <div className="border-t border-slate-800 bg-slate-900/80 backdrop-blur">
319
- <div className="flex items-center justify-between px-4 py-2 border-b border-slate-800">
320
- <div className="flex items-center gap-3">
321
- <div className="flex items-center gap-2">
322
- <Code className="w-4 h-4 text-purple-400" />
323
- <span className="text-sm font-medium text-purple-400">Share Code Snippet</span>
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>
324
384
  </div>
325
- {/* Language selector */}
326
- <div className="relative">
327
- <button
328
- onClick={() => setShowLangDropdown(!showLangDropdown)}
329
- 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"
330
- >
331
- {getLanguageLabel(codeLanguage)}
332
- <ChevronDown className="w-3 h-3 text-slate-400" />
333
- </button>
334
- {showLangDropdown && (
335
- <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">
336
- {CODE_LANGUAGES.map((lang) => (
337
- <button
338
- key={lang.value}
339
- onClick={() => { setCodeLanguage(lang.value); setShowLangDropdown(false); }}
340
- 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"
341
- }`}
342
- >
343
- {lang.label}
344
- </button>
345
- ))}
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)}...
346
413
  </div>
347
414
  )}
348
415
  </div>
349
- </div>
350
- <div className="flex items-center gap-2">
351
- <span className="text-[10px] text-slate-500">⌘+Enter to send</span>
352
- <button
353
- onClick={() => { setShowCodeEditor(false); setCodeContent(""); }}
354
- className="p-1 rounded hover:bg-slate-800 transition-colors"
355
- >
356
- <X className="w-4 h-4 text-slate-400" />
357
- </button>
358
- </div>
416
+ ))}
417
+ <div ref={messagesEndRef} />
359
418
  </div>
360
- <div className="p-3">
361
- <textarea
362
- ref={codeTextareaRef}
363
- value={codeContent}
364
- onChange={(e) => setCodeContent(e.target.value)}
365
- onKeyDown={handleCodeKeyDown}
366
- placeholder="Paste or type your code here..."
367
- spellCheck={false}
368
- 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"
369
- />
370
- <div className="flex justify-end gap-2 mt-2">
371
- <button
372
- onClick={() => { setShowCodeEditor(false); setCodeContent(""); }}
373
- className="px-4 py-2 rounded-lg text-xs text-slate-400 hover:bg-slate-800 transition-colors"
374
- >
375
- Cancel
376
- </button>
377
- <button
378
- onClick={sendCode}
379
- disabled={!connected || !codeContent.trim()}
380
- className="px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-xs font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
381
- >
382
- <Lock className="w-3 h-3" />
383
- Send Encrypted
384
- </button>
419
+
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>
385
457
  </div>
386
- </div>
387
- </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
+ </>
388
469
  )}
389
-
390
- {/* Message input */}
391
- <form onSubmit={sendMessage} className="p-4 border-t border-slate-800 flex gap-3">
392
- <button
393
- type="button"
394
- onClick={openCodeEditor}
395
- disabled={!connected}
396
- 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 disabled:cursor-not-allowed group"
397
- title="Share code snippet"
398
- >
399
- <Code className="w-4 h-4 text-slate-400 group-hover:text-purple-400 transition-colors" />
400
- </button>
401
- <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type an encrypted message..."
402
- disabled={!connected}
403
- 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" />
404
- <button type="submit" disabled={!connected || !input.trim()}
405
- className="px-4 py-2.5 bg-purple-600 hover:bg-purple-500 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed">
406
- <Send className="w-4 h-4" />
407
- </button>
408
- </form>
409
470
  </div>
410
471
  );
411
472
  }