@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.
- package/README.md +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- 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/README.md +1 -1
- 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 +640 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +124 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +350 -76
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +232 -49
- 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 +840 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +589 -123
- package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +4 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +53 -5
- package/templates/nextjs-auth-demo/src/app/layout.tsx +1 -1
- 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
package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { useSession } from "@/lib/auth-client";
|
|
5
|
+
import { connectWithAuth } from "@/lib/pulse";
|
|
6
|
+
import { Lock, Send, Shield, Wifi, WifiOff, Loader2, Key, ShieldCheck, Eye, EyeOff, Code, X, ChevronDown, AlertCircle } from "lucide-react";
|
|
7
|
+
import type { PulseConnection } from "@sansavision/pulse-sdk";
|
|
8
|
+
|
|
9
|
+
interface EncryptedMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
sender: string;
|
|
12
|
+
senderId: string;
|
|
13
|
+
plaintext: string;
|
|
14
|
+
ciphertext: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
type: "text" | "code";
|
|
17
|
+
language?: string;
|
|
18
|
+
}
|
|
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
|
+
|
|
36
|
+
export default function EncryptedChatPage() {
|
|
37
|
+
const { data: session } = useSession();
|
|
38
|
+
const [connected, setConnected] = useState(false);
|
|
39
|
+
const [authUser, setAuthUser] = useState<{ id: string; claims: Record<string, string> } | null>(null);
|
|
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
|
|
50
|
+
const [messages, setMessages] = useState<EncryptedMessage[]>([]);
|
|
51
|
+
const [input, setInput] = useState("");
|
|
52
|
+
const [showCiphertext, setShowCiphertext] = useState(false);
|
|
53
|
+
const [showCodeEditor, setShowCodeEditor] = useState(false);
|
|
54
|
+
const [codeContent, setCodeContent] = useState("");
|
|
55
|
+
const [codeLanguage, setCodeLanguage] = useState("javascript");
|
|
56
|
+
const [showLangDropdown, setShowLangDropdown] = useState(false);
|
|
57
|
+
|
|
58
|
+
const connRef = useRef<PulseConnection | null>(null);
|
|
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]);
|
|
66
|
+
|
|
67
|
+
const scrollToBottom = useCallback(() => {
|
|
68
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
69
|
+
}, [messages]);
|
|
70
|
+
|
|
71
|
+
function simpleEncrypt(text: string, key: string): string {
|
|
72
|
+
return Array.from(text).map((c, i) =>
|
|
73
|
+
(c.charCodeAt(0) ^ key.charCodeAt(i % key.length)).toString(16).padStart(2, "0")
|
|
74
|
+
).join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function simpleDecrypt(hex: string, key: string): string {
|
|
78
|
+
const bytes = hex.match(/.{2}/g) || [];
|
|
79
|
+
return bytes.map((b, i) =>
|
|
80
|
+
String.fromCharCode(parseInt(b, 16) ^ key.charCodeAt(i % key.length))
|
|
81
|
+
).join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Connect to relay
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!session) return;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
|
|
89
|
+
async function init() {
|
|
90
|
+
try {
|
|
91
|
+
const connection = await connectWithAuth();
|
|
92
|
+
if (cancelled) return;
|
|
93
|
+
connRef.current = connection;
|
|
94
|
+
setConnected(true);
|
|
95
|
+
setConnecting(false);
|
|
96
|
+
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
const user = (connection as any).user;
|
|
99
|
+
if (user) setAuthUser(user);
|
|
100
|
+
|
|
101
|
+
connection.on("disconnect", () => setConnected(false));
|
|
102
|
+
connection.on("reconnected", () => setConnected(true));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error("Failed to connect:", err);
|
|
105
|
+
setConnecting(false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
init();
|
|
109
|
+
return () => {
|
|
110
|
+
cancelled = true;
|
|
111
|
+
connRef.current?.disconnect();
|
|
112
|
+
};
|
|
113
|
+
}, [session]);
|
|
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
|
+
|
|
175
|
+
useEffect(scrollToBottom, [messages, scrollToBottom]);
|
|
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
|
+
|
|
206
|
+
function sendMessage(e: React.FormEvent) {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
if (!input.trim() || !streamRef.current || !inRoom) return;
|
|
209
|
+
|
|
210
|
+
const plaintext = input.trim();
|
|
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 }));
|
|
216
|
+
|
|
217
|
+
setMessages((prev) => [
|
|
218
|
+
...prev,
|
|
219
|
+
{ id: crypto.randomUUID(), sender, senderId, plaintext, ciphertext, timestamp: Date.now(), type: "text" },
|
|
220
|
+
]);
|
|
221
|
+
setInput("");
|
|
222
|
+
}
|
|
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
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="h-screen flex flex-col">
|
|
273
|
+
<div className="p-4 border-b border-slate-800">
|
|
274
|
+
<div className="flex items-center justify-between mb-3">
|
|
275
|
+
<div className="flex items-center gap-3">
|
|
276
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-violet-600 flex items-center justify-center">
|
|
277
|
+
<Lock className="w-5 h-5 text-white" />
|
|
278
|
+
</div>
|
|
279
|
+
<div>
|
|
280
|
+
<h1 className="text-lg font-semibold">E2E Encrypted Chat</h1>
|
|
281
|
+
<p className="text-xs text-slate-500">Secure end-to-end messaging</p>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="flex items-center gap-3">
|
|
285
|
+
{authUser && (
|
|
286
|
+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/10 border border-green-500/20">
|
|
287
|
+
<Shield className="w-3.5 h-3.5 text-green-400" />
|
|
288
|
+
<span className="text-xs text-green-400">{authUser.claims.name || authUser.id}</span>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
{connected ? (
|
|
292
|
+
<div className="flex items-center gap-1.5"><Wifi className="w-4 h-4 text-green-400" /><span className="text-xs text-green-400">Connected</span></div>
|
|
293
|
+
) : (
|
|
294
|
+
<div className="flex items-center gap-1.5"><WifiOff className="w-4 h-4 text-red-400" /><span className="text-xs text-red-400">Disconnected</span></div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
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>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
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>
|
|
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>
|
|
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>
|
|
372
|
+
</div>
|
|
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>
|
|
384
|
+
</div>
|
|
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} />
|
|
418
|
+
</div>
|
|
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>
|
|
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
|
+
)}
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|