From a34e1265ff8e32767da8addb70ee17ab5ddab2f9 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Fri, 18 Jul 2025 03:28:45 +0200 Subject: [PATCH] [call-me] #8 - add Searchable user list --- package-lock.json | 4 +- package.json | 2 +- public/client.js | 192 +++++++++++++++++++++++++++++----------------- public/index.html | 29 ++++++- public/style.css | 118 ++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index e89a701..6a94ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "call-me", - "version": "1.0.91", + "version": "1.1.00", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "call-me", - "version": "1.0.91", + "version": "1.1.00", "license": "AGPLv3", "dependencies": { "@ngrok/ngrok": "1.5.1", diff --git a/package.json b/package.json index 3b73bc7..d5ce6b9 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "call-me", - "version": "1.0.91", + "version": "1.1.00", "description": "Your Go-To for Instant Video Calls", "author": "Miroslav Pejic - miroslav.pejic.85@gmail.com", "license": "AGPLv3", diff --git a/public/client.js b/public/client.js index e88ee4e..97651ca 100755 --- a/public/client.js +++ b/public/client.js @@ -20,7 +20,11 @@ const signInPage = document.getElementById('signInPage'); const usernameIn = document.getElementById('usernameIn'); const signInBtn = document.getElementById('signInBtn'); const roomPage = document.getElementById('roomPage'); -const callUsernameSelect = document.getElementById('callUsernameSelect'); +const exitSidebarBtn = document.getElementById('exitSidebarBtn'); +const userSidebar = document.getElementById('userSidebar'); +const userSearchInput = document.getElementById('userSearchInput'); +const userList = document.getElementById('userList'); +const sidebarBtn = document.getElementById('sidebarBtn'); const hideBtn = document.getElementById('hideBtn'); const callBtn = document.getElementById('callBtn'); const swapCameraBtn = document.getElementById('swapCameraBtn'); @@ -45,6 +49,12 @@ let thisConnection; let camera = 'user'; let stream; +// User list state +let userSignedIn = false; +let allConnectedUsers = []; +let filteredUsers = []; +let selectedUser = null; + // Variable to store the interval ID let sessionTimerId = null; @@ -255,8 +265,7 @@ async function handleDirectJoin() { if (call) { // Call user if call is provided setTimeout(() => { - selectIndexByValue(call); - handleCallClick(); + handleUserClickToCall(call); }, 3000); } } @@ -264,27 +273,6 @@ async function handleDirectJoin() { if (!password) await checkHostPassword(); } -// Select index by passed value -function selectIndexByValue(value) { - for (let i = 0; i < callUsernameSelect.options.length; i++) { - if (callUsernameSelect.options[i].value === value) { - callUsernameSelect.selectedIndex = i; // Select the option - break; - } - } -} - -// Remove option by value -function removeOptionByValue(value) { - for (let i = 0; i < callUsernameSelect.options.length; i++) { - if (callUsernameSelect.options[i].value === value) { - alert(value); - callUsernameSelect.remove(i); // Remove the matching option - break; - } - } -} - // Start Session Time function startSessionTime() { console.log('Start session time'); @@ -405,19 +393,64 @@ async function handleEnumerateDevices() { // Handle Listeners function handleListeners() { - // Event listeners signInBtn.addEventListener('click', handleSignInClick); hideBtn.addEventListener('click', toggleLocalVideo); - callBtn.addEventListener('click', handleCallClick); + callBtn.addEventListener('click', handleCallBtnClick); videoBtn.addEventListener('click', handleVideoClick); audioBtn.addEventListener('click', handleAudioClick); hangUpBtn.addEventListener('click', handleHangUpClick); + exitSidebarBtn.addEventListener('click', handleExitSidebarClick); localVideoContainer.addEventListener('click', toggleFullScreen); remoteVideo.addEventListener('click', toggleFullScreen); - // Add keyUp listeners - callUsernameSelect.addEventListener('keyup', (e) => handleKeyUp(e, handleCallClick)); - callUsernameSelect.addEventListener('change', (e) => handleChangeUserToCall(e)); usernameIn.addEventListener('keyup', (e) => handleKeyUp(e, handleSignInClick)); + + // Sidebar toggle + if (sidebarBtn && userSidebar) { + sidebarBtn.addEventListener('click', (e) => { + e.stopPropagation(); + userSidebar.classList.toggle('active'); + }); + + document.addEventListener('click', (e) => { + if (window.innerWidth > 768) return; // Ignore clicks on desktop + let el = e.target; + let shouldExclude = false; + while (el) { + if (el instanceof HTMLElement && (el.id === 'userSidebar' || el.id === 'sidebarBtn')) { + shouldExclude = true; + break; + } + el = el.parentElement; + } + if (!shouldExclude && userSidebar.classList.contains('active')) { + userSidebar.classList.remove('active'); + } + }); + } +} + +// Hide sidebar after user selection (on mobile) +function handleUserClickToCall(user) { + if (!user) { + handleError('No user selected.'); + return; + } + if (user === userName) { + handleError('You cannot call yourself.'); + return; + } + selectedUser = user; + renderUserList(); + connectedUser = user; + sendMsg({ + type: 'offerAccept', + from: userName, + to: user, + }); + popupMsg(`You are calling ${user}.
Please wait for them to answer.`); + if (userSidebar.classList.contains('active')) { + userSidebar.classList.remove('active'); + } } // Handle element display @@ -457,34 +490,9 @@ function toggleLocalVideo() { } } -// Handle Select user to call on changes -function handleChangeUserToCall(e) { - const selectedValue = e.target.value; - if (selectedValue) { - console.log(`You selected: ${selectedValue}`); - if (!callBtn.classList.contains('pulsate')) callBtn.classList.add('pulsate'); - } -} - // Handle call button click -function handleCallClick() { - const callToUsername = callUsernameSelect.value.trim(); - if (callToUsername.length > 0) { - if (callToUsername === userName) { - handleError('You cannot call yourself.'); - return; - } - connectedUser = callToUsername; - sendMsg({ - type: 'offerAccept', - from: userName, - to: callToUsername, - }); - popupMsg(`You are calling ${callToUsername}.
Please wait for them to answer.`); - if (callBtn.classList.contains('pulsate')) callBtn.classList.remove('pulsate'); - } else { - handleError('Please select the user to call.'); - } +function handleCallBtnClick() { + handleUserClickToCall(selectedUser); } // Toggle video stream @@ -614,6 +622,13 @@ function handleHangUpClick() { handleLeave(); } +// Handle leaving the call +function handleExitSidebarClick() { + if (userSidebar.classList.contains('active')) { + userSidebar.classList.remove('active'); + } +} + // Toggle video full screen mode function toggleFullScreen(e) { if (!e.target.srcObject) return; @@ -639,7 +654,9 @@ function handlePing(data) { function handleNotFound(data) { const { username } = data; handleError(`Username ${username} not found!`); - removeOptionByValue(username); + // Remove from user list if present + allConnectedUsers = allConnectedUsers.filter((u) => u !== username); + filterUserList(userSearchInput.value || ''); } // Handle sign-in response from the server @@ -651,6 +668,11 @@ async function handleSignIn(data) { setTimeout(handleHangUpClick, 3000); } } else { + userSignedIn = true; + + if (userInfo.device.isDesktop) userSidebar.classList.toggle('active'); + if (userInfo.device.isMobile) userSidebar.style.width = '100%'; + elemDisplay(githubDiv, false); elemDisplay(attribution, false); elemDisplay(signInPage, false); @@ -875,21 +897,9 @@ async function handleCandidate(data) { // Handle connected users function handleUsers(data) { - console.log('Connected users ------>', data.users); - callUsernameSelect.innerHTML = ''; - data.users.forEach((user) => { - if (user === userName) return; - const option = document.createElement('option'); - option.value = user; - option.textContent = user; - callUsernameSelect.appendChild(option); - }); - if (callUsernameSelect.options.length === 0) { - callUsernameSelect.innerHTML = ''; - if (callBtn.classList.contains('pulsate')) callBtn.classList.remove('pulsate'); - } else { - if (!callBtn.classList.contains('pulsate')) callBtn.classList.add('pulsate'); - } + allConnectedUsers = data.users.filter((u) => u !== userName); + filterUserList(userSearchInput.value || ''); + callBtn.classList.toggle('pulsate', allConnectedUsers.length > 0); } // Handle remote video status @@ -1000,6 +1010,44 @@ function sendMsg(message) { socket.emit('message', message); } +// Select user by value in the user list +function renderUserList() { + userList.innerHTML = ''; + filteredUsers.forEach((user) => { + const li = document.createElement('li'); + li.textContent = user; + li.tabIndex = 0; + if (user === selectedUser) li.classList.add('selected'); + li.addEventListener('click', () => { + if (!userSignedIn) return; + selectedUser = user; + renderUserList(); + }); + li.addEventListener('dblclick', () => { + if (!userSignedIn) return; + handleUserClickToCall(user); + }); + li.addEventListener('keydown', (e) => { + if (!userSignedIn) return; + if (e.key === 'Enter') handleUserClickToCall(user); + }); + userList.appendChild(li); + }); +} + +// Filter user list based on search input +function filterUserList(query) { + filteredUsers = allConnectedUsers.filter((u) => u.toLowerCase().includes(query.toLowerCase())); + // If selected user is filtered out, deselect + if (!filteredUsers.includes(selectedUser)) selectedUser = null; + renderUserList(); +} + +// Handle user search input +userSearchInput?.addEventListener('input', (e) => { + filterUserList(e.target.value); +}); + // Clean up before window close or reload window.onbeforeunload = () => { handleHangUpClick(); diff --git a/public/index.html b/public/index.html index b0d4d39..d825a9d 100755 --- a/public/index.html +++ b/public/index.html @@ -58,6 +58,20 @@ + +
+
+ Connected Users + +
+ + +
+
@@ -102,10 +116,6 @@
- - + + +
diff --git a/public/style.css b/public/style.css index b75573e..27faefd 100644 --- a/public/style.css +++ b/public/style.css @@ -308,6 +308,124 @@ select::-ms-expand { right: 10px; } +/* User Sidebar Styles */ +.user-sidebar { + position: fixed; + display: flex; + top: 0; + right: 0; + width: 320px; + height: 100vh; + background: rgba(30, 32, 36, 0.98); + border-left: 2px solid #333; + flex-direction: column; + z-index: 1002; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2); + transition: + transform 0.3s ease, + opacity 0.3s; + transform: translateX(100%); + opacity: 0; + pointer-events: none; +} + +.user-sidebar.active { + transform: translateX(0); + opacity: 1; + pointer-events: auto; + box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25); + animation: sidebarFadeIn 0.5s ease; +} + +@media (max-width: 768px) { + .user-sidebar.active { + animation: none !important; + } +} + +@keyframes sidebarFadeIn { + from { + opacity: 0; + transform: translateX(40px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* User Sidebar Header */ +.user-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem 0.5rem 1rem; + border-bottom: 1px solid #333; + background: rgba(30, 32, 36, 0.98); +} +.user-sidebar-title { + font-weight: bold; + font-size: 1rem; + color: #fff; +} +.btn-exit-sidebar { + background: none; + border: none; + color: #fff; + font-size: 1.3rem; + padding: 0.2rem 0.5rem; + cursor: pointer; + transition: color 0.2s; +} +.btn-exit-sidebar:hover { + color: #d9534f; +} + +/* User Search Bar */ +.user-search-bar { + padding: 16px 16px 8px 16px; + background: #23242a; + border-bottom: 1px solid #333; +} + +#userSearchInput { + width: 100%; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid #444; + background: #18191c; + color: #fff; + font-size: 1em; +} + +/* User List Styles */ +.user-list { + flex: 1; + margin: 0; + padding: 0 0 16px 0; + list-style: none; + overflow-y: auto; +} + +.user-list li { + padding: 12px 20px; + border-bottom: 1px solid #292a2e; + color: #fff; + cursor: pointer; + transition: background 0.15s; + display: flex; + align-items: center; + font-size: 1.05em; +} +.user-list li:hover { + background: #2a2b31; + color: #ffd700; +} +.user-list li.selected { + background: #444; + color: #ffd700; +} + /* Swal2 custom theme */ .swal2-popup { background: rgba(0, 0, 0, 0.5) !important;