@sansavision/create-pulse 0.4.4 → 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.
- package/package.json +2 -2
- package/templates/aurora-auth-node-demo/README.md +43 -0
- package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
- package/templates/aurora-auth-node-demo/bun.lock +679 -0
- package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
- package/templates/aurora-auth-node-demo/package.json +39 -0
- package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/aurora-auth-node-demo/server.mjs +46 -0
- package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
- package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
- package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
- package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
- package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
- package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
- package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
- package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
- package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
- package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
- package/templates/nextjs-auth-demo/package.json +8 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
- package/templates/nextjs-auth-node-demo/.env.example +10 -0
- package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
- package/templates/nextjs-auth-node-demo/README.md +159 -0
- package/templates/nextjs-auth-node-demo/_gitignore +33 -0
- package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-node-demo/package.json +38 -0
- package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-node-demo/server.mjs +45 -0
- package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
- package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
- 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
|
-
|
|
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() || !
|
|
208
|
+
if (!input.trim() || !streamRef.current || !inRoom) return;
|
|
135
209
|
|
|
136
210
|
const plaintext = input.trim();
|
|
137
|
-
const ciphertext = simpleEncrypt(plaintext,
|
|
211
|
+
const ciphertext = simpleEncrypt(plaintext, roomId);
|
|
138
212
|
const sender = session?.user?.name || "Anonymous";
|
|
213
|
+
const senderId = session?.user?.id || "unknown";
|
|
139
214
|
|
|
140
|
-
|
|
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() || !
|
|
225
|
+
if (!codeContent.trim() || !streamRef.current || !inRoom) return;
|
|
160
226
|
|
|
161
227
|
const plaintext = codeContent.trim();
|
|
162
|
-
const ciphertext = simpleEncrypt(plaintext,
|
|
228
|
+
const ciphertext = simpleEncrypt(plaintext, roomId);
|
|
163
229
|
const sender = session?.user?.name || "Anonymous";
|
|
230
|
+
const senderId = session?.user?.id || "unknown";
|
|
164
231
|
|
|
165
|
-
|
|
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
|
-
|
|
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">
|
|
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
|
-
|
|
248
|
-
<div className="flex items-center gap-
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
350
|
-
<div
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
}
|