[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { XCUQuantumCipher } from "../lib/xcu-quantum-cipher";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
sender: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
isSelf: boolean;
|
||||
isResonanceAudio?: boolean;
|
||||
}
|
||||
|
||||
export const JumlahChat = ({
|
||||
roomName,
|
||||
participantName,
|
||||
participantId,
|
||||
webTransport,
|
||||
onClose,
|
||||
}: {
|
||||
roomName: string;
|
||||
participantName: string;
|
||||
participantId: number;
|
||||
webTransport: { datagrams: { readable: ReadableStream, writable: WritableStream } } | null;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [typingHeat, setTypingHeat] = useState(0);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
const cipherRef = useRef<XCUQuantumCipher | null>(null);
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
const typingTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioChunksRef = useRef<Blob[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const initCipher = async () => {
|
||||
const cipher = new XCUQuantumCipher(roomName);
|
||||
await cipher.initialize();
|
||||
cipherRef.current = cipher;
|
||||
};
|
||||
initCipher();
|
||||
}, [roomName]);
|
||||
|
||||
useEffect(() => {
|
||||
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages, typingHeat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webTransport || !webTransport.datagrams) return;
|
||||
let isActive = true;
|
||||
const readDatagrams = async () => {
|
||||
try {
|
||||
const reader = webTransport.datagrams.readable.getReader();
|
||||
while (isActive) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value && value.length >= 8) {
|
||||
const type = value[0];
|
||||
const senderId = new DataView(value.buffer).getUint16(2, true);
|
||||
if (senderId === participantId) continue;
|
||||
const payload = value.slice(8);
|
||||
if (type === 7 && cipherRef.current) {
|
||||
try {
|
||||
const dec = await cipherRef.current.decrypt(payload);
|
||||
const parsed = JSON.parse(dec);
|
||||
setMessages((prev) => [...prev, { id: crypto.randomUUID(), sender: parsed.sender, text: parsed.text, timestamp: parsed.timestamp, isSelf: false }]);
|
||||
} catch (e) {}
|
||||
} else if (type === 8) {
|
||||
setTypingHeat((prev) => Math.min(prev + 20, 100));
|
||||
if (typingTimeout.current) clearTimeout(typingTimeout.current);
|
||||
typingTimeout.current = setTimeout(() => setTypingHeat(0), 1000);
|
||||
} else if (type === 9 && cipherRef.current) {
|
||||
try {
|
||||
const dec = await cipherRef.current.decrypt(payload);
|
||||
const parsed = JSON.parse(dec);
|
||||
setMessages((prev) => [...prev, { id: crypto.randomUUID(), sender: parsed.sender, text: parsed.audioBase64, timestamp: parsed.timestamp, isSelf: false, isResonanceAudio: true }]);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
readDatagrams();
|
||||
return () => { isActive = false; };
|
||||
}, [webTransport, participantId]);
|
||||
|
||||
const sendTypingResonance = async () => {
|
||||
if (!webTransport || !webTransport.datagrams) return;
|
||||
let writer: WritableStreamDefaultWriter | null = null;
|
||||
try {
|
||||
writer = webTransport.datagrams.writable.getWriter();
|
||||
const buf = new Uint8Array(8);
|
||||
buf[0] = 8; new DataView(buf.buffer).setUint16(2, participantId, true);
|
||||
await writer.write(buf);
|
||||
} catch {} finally { if (writer) writer.releaseLock(); }
|
||||
};
|
||||
|
||||
const sendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || !cipherRef.current || !webTransport) return;
|
||||
const payloadStr = JSON.stringify({ sender: participantName, text: input, timestamp: Date.now() });
|
||||
const encPayload = await cipherRef.current.encrypt(payloadStr);
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 7; new DataView(header.buffer).setUint16(2, participantId, true);
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0); fullPacket.set(encPayload, 8);
|
||||
let writer: WritableStreamDefaultWriter | null = null;
|
||||
try {
|
||||
writer = webTransport.datagrams.writable.getWriter();
|
||||
if (writer) await writer.write(fullPacket);
|
||||
setMessages((prev) => [...prev, { id: crypto.randomUUID(), sender: participantName, text: input, timestamp: Date.now(), isSelf: true }]);
|
||||
} catch (e) {} finally { if (writer) writer.releaseLock(); }
|
||||
setInput("");
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
|
||||
mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = [];
|
||||
mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunksRef.current.push(e.data); };
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(audioBlob);
|
||||
reader.onloadend = async () => {
|
||||
const base64data = reader.result as string;
|
||||
if (cipherRef.current && webTransport) {
|
||||
const payloadStr = JSON.stringify({ sender: participantName, audioBase64: base64data, timestamp: Date.now() });
|
||||
const encPayload = await cipherRef.current.encrypt(payloadStr);
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 9; new DataView(header.buffer).setUint16(2, participantId, true);
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0); fullPacket.set(encPayload, 8);
|
||||
let writer: WritableStreamDefaultWriter | null = null;
|
||||
try {
|
||||
writer = webTransport.datagrams.writable.getWriter();
|
||||
if (writer) await writer.write(fullPacket);
|
||||
} catch (e) {} finally { if (writer) writer.releaseLock(); }
|
||||
setMessages((prev) => [...prev, { id: crypto.randomUUID(), sender: participantName, text: base64data, timestamp: Date.now(), isSelf: true, isResonanceAudio: true }]);
|
||||
}
|
||||
};
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
};
|
||||
mediaRecorder.start(); setIsRecording(true);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); } };
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-transparent">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-black/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-sm font-black uppercase tracking-widest text-white/90">In-Meeting Chat</h2>
|
||||
<span className="px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">BYOK XChaCha20</span>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-white transition-colors">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 p-6 overflow-y-auto custom-scroll flex flex-col gap-6">
|
||||
{messages.length === 0 ? (
|
||||
<div className="my-auto text-center space-y-4">
|
||||
<div className="w-12 h-12 bg-emerald-500/10 rounded-2xl flex items-center justify-center mx-auto text-emerald-500">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 uppercase font-black tracking-widest leading-relaxed">
|
||||
Kanal Transmisi Kuantum Aktif.<br />BYOK XChaCha20-Poly1305 E2EE.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex flex-col ${msg.isSelf ? "items-end" : "items-start"}`}>
|
||||
<div className="flex items-center gap-2 mb-1.5 px-1">
|
||||
<span className="text-[9px] font-black uppercase tracking-widest text-white/40">{msg.sender}</span>
|
||||
<span className="text-[8px] font-mono text-white/20">{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
<div className={`px-4 py-3 rounded-2xl text-[13px] leading-relaxed max-w-[90%] shadow-xl border ${msg.isSelf ? "bg-emerald-600 border-emerald-500 text-black font-medium rounded-tr-none" : "bg-white/5 border-white/5 text-white/90 rounded-tl-none"}`}>
|
||||
{msg.isResonanceAudio ? (
|
||||
<audio controls src={msg.text} className="h-8 w-44 filter invert brightness-200" />
|
||||
) : (
|
||||
msg.text
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Telepathic Resonance Heatmap */}
|
||||
{typingHeat > 0 && (
|
||||
<div className="flex items-center gap-3 px-1">
|
||||
<span className="text-[9px] font-black text-emerald-500 uppercase tracking-widest animate-pulse">Partisipan Mengetik</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="w-1 bg-emerald-500 rounded-full animate-bounce" style={{ height: '8px', animationDelay: `${i * 0.1}s` }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-6 border-t border-white/5">
|
||||
<form onSubmit={sendMessage} className="relative group">
|
||||
<input
|
||||
type="text" value={input} onChange={(e) => { setInput(e.target.value); sendTypingResonance(); }}
|
||||
placeholder="Ketik pesan..."
|
||||
className="w-full bg-white/5 border border-white/5 rounded-2xl pl-4 pr-24 py-4 text-xs text-white placeholder-white/20 focus:outline-none focus:border-emerald-500/50 focus:bg-white/10 transition-all shadow-inner"
|
||||
/>
|
||||
<div className="absolute right-2 top-2 bottom-2 flex items-center gap-1">
|
||||
<button
|
||||
type="button" onMouseDown={startRecording} onMouseUp={stopRecording} onMouseLeave={stopRecording}
|
||||
className={`p-2.5 rounded-xl transition-all ${isRecording ? "bg-red-500 animate-pulse scale-110 shadow-lg" : "text-white/40 hover:text-white hover:bg-white/5"}`}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
|
||||
</button>
|
||||
<button
|
||||
type="submit" disabled={!input.trim()}
|
||||
className="bg-emerald-500 hover:bg-emerald-400 disabled:opacity-20 text-black p-2.5 rounded-xl transition-all shadow-lg"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-[8px] text-center text-white/20 mt-4 uppercase font-black tracking-widest">Quantum Resonance Encryption Active</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user