Add files via upload

This commit is contained in:
Cris Charles authored and GitHub committed 2026-01-07 05:25:30 +08:00
commit eed97bff3a
5 files changed
+1693

No files matched your search

+361
View File
@@ -0,0 +1,361 @@
const socket = io();
// DOM Elements
const landingPage = document.getElementById('landing-page');
const mainApp = document.getElementById('main-app');
const videoContainer = document.getElementById('video-container');
const messagesDiv = document.getElementById('messages');
const msgInput = document.getElementById('msgInput');
const sendBtn = document.getElementById('sendBtn');
const actionBtn = document.getElementById('actionBtn');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const remotePlaceholder = document.getElementById('remote-placeholder');
// State
let currentMode = null; // 'text' or 'video'
let isConnected = false;
let currentRoomId = null;
let peerConnection = null;
let localStream = null;
// WebRTC Config
const rtcConfig = {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
};
// ---------------- UI NAVIGATION ----------------
async function startChat(mode) {
currentMode = mode;
// Hide Landing, Show App
landingPage.classList.add('hidden');
mainApp.classList.remove('hidden');
mainApp.classList.add('flex'); // Ensure flex layout applies
// Mode Specific Layout
if (mode === 'text') {
videoContainer.classList.add('hidden'); // STRICTLY HIDE VIDEO
videoContainer.classList.remove('flex');
} else {
videoContainer.classList.remove('hidden');
videoContainer.classList.add('flex'); // Show Video
await setupMedia(); // Get Camera access
}
// Immediately trigger "New" search
handleAction();
}
function goHome() {
location.reload();
}
// ---------------- DEVICE HANDLING ----------------
async function setupMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localStream = stream;
localVideo.srcObject = stream;
populateDevices();
} catch (err) {
alert("Camera access denied or missing.");
goHome();
}
}
async function populateDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoSelect = document.getElementById('camSelect');
const audioSelect = document.getElementById('micSelect');
videoSelect.innerHTML = '';
audioSelect.innerHTML = '';
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `${device.kind} - ${device.deviceId.slice(0,5)}`;
if (device.kind === 'videoinput') videoSelect.appendChild(option);
if (device.kind === 'audioinput') audioSelect.appendChild(option);
});
}
function toggleMirror() {
localVideo.classList.toggle('mirror');
}
// ---------------- CONNECTION LOGIC (STOP / NEW) ----------------
function handleAction() {
if (isConnected || actionBtn.innerText === "STOP") {
// --- USER CLICKED STOP ---
disconnect();
actionBtn.innerText = "NEW";
actionBtn.className = "bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition flex items-center gap-2 min-w-[100px] justify-center";
systemMessage("You stopped the chat.");
} else {
// --- USER CLICKED NEW ---
findStranger();
}
}
function findStranger() {
messagesDiv.innerHTML = ''; // Clear chat
systemMessage("Looking for a stranger...");
actionBtn.innerText = "STOP";
actionBtn.className = "bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition flex items-center gap-2 min-w-[100px] justify-center";
const interests = document.getElementById('interestInput').value;
socket.emit('find_partner', { mode: currentMode, interests });
}
function disconnect() {
isConnected = false;
currentRoomId = null;
// Disable inputs
msgInput.disabled = true;
sendBtn.disabled = true;
// Cleanup WebRTC
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
remoteVideo.srcObject = null;
if(remotePlaceholder) remotePlaceholder.style.display = 'flex';
socket.emit('leave_room', currentRoomId);
}
// ---------------- SOCKET EVENTS ----------------
socket.on('update_count', (count) => {
document.getElementById('online-count-landing').innerText = count;
document.getElementById('chat-online-count').innerText = `${count} online`;
});
socket.on('waiting', (data) => {
systemMessage(data.message);
});
socket.on('match_found', ({ roomId }) => {
isConnected = true;
currentRoomId = roomId;
systemMessage("Stranger connected! Say hi.");
msgInput.disabled = false;
sendBtn.disabled = false;
msgInput.focus();
if (currentMode === 'video') {
remotePlaceholder.style.display = 'none';
}
});
socket.on('partner_disconnected', () => {
systemMessage("Stranger has disconnected.");
disconnect(); // Use local disconnect logic to reset state
// Automatically reset button to NEW
actionBtn.innerText = "NEW";
actionBtn.className = "bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition flex items-center gap-2 min-w-[100px] justify-center";
});
// ---------------- WEBRTC LOGIC (Video Only) ----------------
socket.on('make_offer', async () => {
createPeerConnection();
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.emit('signal', { room: currentRoomId, type: 'offer', sdp: offer });
});
socket.on('signal', async (data) => {
if (!peerConnection) createPeerConnection();
if (data.type === 'offer') {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('signal', { room: currentRoomId, type: 'answer', sdp: answer });
}
else if (data.type === 'answer') {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
}
else if (data.candidate) {
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
});
function createPeerConnection() {
peerConnection = new RTCPeerConnection(rtcConfig);
// Add local tracks
if (localStream) {
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
}
// Handle remote tracks
peerConnection.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
// Ice Candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('signal', { room: currentRoomId, candidate: event.candidate });
}
};
}
// ---------------- CHAT LOGIC ----------------
function sendMessage() {
const text = msgInput.value.trim();
if (!text) return;
appendMessage('You', text, 'self');
socket.emit('message', { room: currentRoomId, text });
msgInput.value = '';
}
socket.on('message', (data) => {
appendMessage('Stranger', data.text, 'stranger');
});
// Helper: Append Message
function appendMessage(sender, text, type) {
const div = document.createElement('div');
// Styling based on sender
const alignClass = type === 'self' ? 'self-end bg-blue-600 text-white' : 'self-start bg-slate-700 text-slate-200';
div.className = `max-w-[80%] px-4 py-2 rounded-lg text-sm break-words ${alignClass}`;
div.innerHTML = `<strong>${sender}:</strong> ${text}`; // No redundant report button here
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function systemMessage(text) {
const div = document.createElement('div');
div.className = "text-center text-slate-500 text-xs italic my-2";
div.innerText = text;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function reportUser() {
if(!isConnected) return;
alert("User reported to administration.");
}
// --- DRAG AND RESIZE LOGIC ---
const wrapper = document.getElementById('localVideoWrapper');
const resizeHandle = document.getElementById('resizeHandle');
const container = document.getElementById('video-container');
// State variables
let isDragging = false;
let isResizing = false;
let startX, startY, startLeft, startTop, startWidth, startHeight;
// 1. DRAG FUNCTIONALITY
wrapper.addEventListener('mousedown', (e) => {
// If clicking the resize handle or controls, don't drag
if (e.target === resizeHandle || e.target.closest('select') || e.target.closest('button')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
// Get current position
const rect = wrapper.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
// Calculate 'left' and 'top' relative to the container
startLeft = rect.left - containerRect.left;
startTop = rect.top - containerRect.top;
document.body.classList.add('noselect'); // Prevent highlighting text
});
// 2. RESIZE FUNCTIONALITY
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
e.stopPropagation(); // Stop the drag event from firing
startX = e.clientX;
startY = e.clientY;
const rect = wrapper.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
document.body.classList.add('noselect');
});
// 3. GLOBAL MOUSE MOVE (Handles both)
document.addEventListener('mousemove', (e) => {
if (!isDragging && !isResizing) return;
e.preventDefault();
if (isDragging) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Calculate new position
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Boundaries (Don't let it leave the container)
const containerRect = container.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
const maxLeft = containerRect.width - wrapperRect.width;
const maxTop = containerRect.height - wrapperRect.height;
// Clamp values
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
if (newLeft > maxLeft) newLeft = maxLeft;
if (newTop > maxTop) newTop = maxTop;
wrapper.style.left = `${newLeft}px`;
wrapper.style.top = `${newTop}px`;
// Remove 'bottom' and 'right' classes from Tailwind if they exist
// because we are now using explicit top/left
wrapper.style.bottom = 'auto';
wrapper.style.right = 'auto';
}
if (isResizing) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newWidth = startWidth + dx;
let newHeight = startHeight + dy;
// Minimum size constraints
if (newWidth < 100) newWidth = 100;
if (newHeight < 75) newHeight = 75;
wrapper.style.width = `${newWidth}px`;
wrapper.style.height = `${newHeight}px`;
}
});
// 4. MOUSE UP (Stop everything)
document.addEventListener('mouseup', () => {
isDragging = false;
isResizing = false;
document.body.classList.remove('noselect');
});
// Enter key to send
msgInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage() });
+167
View File
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StrangerTalk - Connect Freely</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="/socket.io/socket.io.js"></script>
<style>
/* Custom Tweaks */
body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: white; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
/* Video Mirroring */
.mirror { transform: scaleX(-1); }
/* Range Slider Style */
input[type=range] { accent-color: #3b82f6; }
/* Prevent text selection while dragging */
.noselect {
user-select: none;
-webkit-user-select: none;
}
/* Style for the resize handle area */
#resizeHandle {
/* Triangle shape using clip-path or just a square */
clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
</style>
</head>
<body class="h-screen w-screen overflow-hidden flex flex-col">
<div id="landing-page" class="flex-1 flex flex-col items-center justify-center p-6 relative bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
<h1 class="text-5xl md:text-7xl font-bold tracking-tighter mb-4 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-600">
StrangerTalk
</h1>
<p class="text-slate-400 text-lg mb-8 max-w-md text-center">
Talk to strangers, make friends, or just pass the time. Choose your mode below.
</p>
<div class="w-full max-w-md mb-6">
<label class="text-xs uppercase font-bold text-slate-500 ml-1">Interests (Optional)</label>
<input id="interestInput" type="text" placeholder="e.g. Anime, Coding, Music"
class="w-full mt-1 px-4 py-3 rounded-xl bg-slate-800 border border-slate-700 focus:border-blue-500 outline-none transition text-white">
</div>
<div class="flex gap-4 mb-12">
<button onclick="startChat('text')" class="px-8 py-4 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 transition flex items-center gap-2 font-bold shadow-lg">
<span class="material-icons text-blue-400">chat</span> Text Only
</button>
<button onclick="startChat('video')" class="px-8 py-4 rounded-xl bg-blue-600 hover:bg-blue-500 transition flex items-center gap-2 font-bold shadow-lg shadow-blue-900/50">
<span class="material-icons text-white">videocam</span> Video Chat
</button>
</div>
<div class="absolute top-6 right-6 flex items-center gap-2 bg-slate-800 px-3 py-1 rounded-full text-xs">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span id="online-count-landing">0</span> Online
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-center max-w-4xl text-slate-400 text-sm mt-8">
<div class="p-4 bg-slate-800/50 rounded-lg">
<h3 class="font-bold text-white mb-2">Is it free?</h3>
<p>Yes, StrangerTalk is completely free to use instantly.</p>
</div>
<div class="p-4 bg-slate-800/50 rounded-lg">
<h3 class="font-bold text-white mb-2">Is it safe?</h3>
<p>We use moderation tools, but always be careful sharing info.</p>
</div>
<div class="p-4 bg-slate-800/50 rounded-lg">
<h3 class="font-bold text-white mb-2">Do I need an account?</h3>
<p>No registration required. Just click start.</p>
</div>
</div>
<footer class="absolute bottom-4 text-xs text-slate-600 flex gap-4">
<span>© 2026 StrangerTalk</span>
<a href="#" class="hover:text-slate-400">Blog</a>
<a href="#" class="hover:text-slate-400">Rules</a>
<a href="#" class="hover:text-slate-400">Privacy</a>
</footer>
</div>
<div id="main-app" class="hidden h-full flex flex-col md:flex-row">
<div id="video-container" class="hidden md:flex-1 relative bg-black flex flex-col h-[50vh] md:h-auto border-b md:border-b-0 md:border-r border-slate-800">
<div class="relative flex-1 w-full h-full overflow-hidden group">
<video id="remoteVideo" autoplay playsinline class="w-full h-full object-cover"></video>
<div class="absolute inset-0 flex items-center justify-center text-slate-500" id="remote-placeholder">
<span class="animate-pulse">Waiting for partner...</span>
</div>
<div class="absolute top-4 left-4 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition duration-300">
<button class="bg-red-600/80 hover:bg-red-600 text-white p-2 rounded-full text-xs flex items-center gap-1 backdrop-blur-sm">
<span class="material-icons text-sm">flag</span> Report
</button>
<div class="bg-black/60 p-2 rounded-full flex items-center gap-2 backdrop-blur-sm">
<span class="material-icons text-sm">volume_up</span>
<input type="range" min="0" max="1" step="0.1" class="w-20 h-1" oninput="document.getElementById('remoteVideo').volume = this.value">
</div>
</div>
</div>
<div id="localVideoWrapper" class="absolute bottom-4 right-4 w-48 h-36 bg-slate-900 rounded-lg shadow-2xl z-30 group cursor-move overflow-hidden border-2 border-slate-700 min-w-[100px] min-h-[75px]">
<video id="localVideo" autoplay muted playsinline class="w-full h-full object-cover pointer-events-none mirror"></video>
<div class="absolute inset-0 bg-black/60 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition duration-200 gap-2 p-2 pointer-events-auto">
<button id="flipBtn" onclick="toggleMirror()" class="text-xs bg-slate-700 px-2 py-1 rounded text-white flex items-center gap-1 hover:bg-slate-600 w-full justify-center">
<span class="material-icons text-xs">flip</span> Flip
</button>
<select id="camSelect" class="w-full text-[10px] bg-slate-800 text-white border-none rounded p-1 outline-none">
<option>Default Camera</option>
</select>
<select id="micSelect" class="w-full text-[10px] bg-slate-800 text-white border-none rounded p-1 outline-none">
<option>Default Mic</option>
</select>
</div>
<div id="resizeHandle" class="absolute bottom-0 right-0 w-4 h-4 bg-blue-500 cursor-nwse-resize z-40 rounded-tl opacity-0 group-hover:opacity-100 transition"></div>
</div>
</div>
<div id="chat-container" class="flex-1 md:w-[400px] md:max-w-[400px] flex flex-col bg-white dark:bg-slate-900 relative">
<div class="h-16 border-b border-slate-800 flex items-center justify-between px-4 bg-slate-900 z-10">
<div class="flex items-center gap-2">
<button onclick="goHome()" class="font-bold text-xl tracking-tighter hover:text-blue-400 transition">StrangerTalk</button>
<span class="text-xs bg-slate-800 px-2 py-0.5 rounded text-slate-400" id="chat-online-count">0 online</span>
</div>
<button onclick="reportUser()" class="text-slate-400 hover:text-red-500 transition text-sm flex items-center gap-1">
<span class="material-icons text-sm">flag</span> Report
</button>
</div>
<div id="messages" class="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-900">
<div class="bg-blue-900/20 border border-blue-900/50 p-3 rounded-lg text-xs text-blue-200 mb-4">
<strong>Note:</strong> Be polite. Keep it clean. Violators will be banned.
</div>
<div class="text-center text-slate-600 text-sm italic" id="status-msg">
Click "Start" to find a stranger.
</div>
</div>
<div class="p-4 bg-slate-900 border-t border-slate-800">
<div class="flex gap-2">
<button id="actionBtn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition flex items-center gap-2 min-w-[100px] justify-center" onclick="handleAction()">
NEW
</button>
<input id="msgInput" type="text" placeholder="Type a message..." disabled
class="flex-1 bg-slate-800 text-white px-4 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 transition disabled:opacity-50">
<button id="sendBtn" class="bg-slate-700 hover:bg-blue-600 text-white p-3 rounded-lg transition disabled:opacity-50" disabled onclick="sendMessage()">
<span class="material-icons">send</span>
</button>
</div>
</div>
</div>
</div>
<script src="client.js"></script>
</body>
</html>
+1066
View File
File diff suppressed because it is too large. Load diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "glimpse",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^5.2.1",
"socket.io": "^4.8.3"
}
}
+82
View File
@@ -0,0 +1,82 @@
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static('public'));
let textQueue = [];
let videoQueue = [];
let onlineUsers = 0;
io.on('connection', (socket) => {
onlineUsers++;
io.emit('update_count', onlineUsers);
socket.on('find_partner', ({ mode, interests }) => {
socket.mode = mode; // 'text' or 'video'
// Simple Queue Logic
let queue = mode === 'text' ? textQueue : videoQueue;
if (queue.length > 0) {
// Match found!
const partner = queue.pop();
const roomId = `room_${socket.id}_${partner.id}`;
socket.join(roomId);
partner.join(roomId);
// Notify both
io.to(roomId).emit('match_found', { roomId });
// If video, tell them who is initiating
if (mode === 'video') {
socket.emit('make_offer', { roomId }); // One side starts WebRTC
}
} else {
// No match, wait in queue
queue.push(socket);
socket.emit('waiting', { message: `Looking for a ${mode} partner...` });
}
});
// WebRTC Signaling (Video Mode)
socket.on('signal', (data) => {
socket.to(data.room).emit('signal', data);
});
// Chat Messages
socket.on('message', (data) => {
socket.to(data.room).emit('message', {
text: data.text,
sender: 'stranger'
});
});
// Handle Stop/Disconnect
socket.on('leave_room', (roomId) => {
if(roomId) {
socket.to(roomId).emit('partner_disconnected');
socket.leave(roomId);
}
// Remove from queues if they were waiting
textQueue = textQueue.filter(s => s.id !== socket.id);
videoQueue = videoQueue.filter(s => s.id !== socket.id);
});
socket.on('disconnect', () => {
onlineUsers--;
io.emit('update_count', onlineUsers);
// Clean up queues
textQueue = textQueue.filter(s => s.id !== socket.id);
videoQueue = videoQueue.filter(s => s.id !== socket.id);
});
});
server.listen(3002, () => {
console.log('Server running on http://localhost:3002');
});