[call-me] #8 - add Searchable user list

This commit is contained in:
Miroslav Pejic
2025-07-18 03:28:45 +02:00
parent e6af5839e0
commit a34e1265ff
5 changed files with 266 additions and 79 deletions
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+120 -72
View File
@@ -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}.<br/>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}.<br/>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 = '<option value="" disabled selected>Select a user to call</option>';
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();
+25 -4
View File
@@ -58,6 +58,20 @@
</a>
</div>
<!-- Sidebar for user list -->
<div id="userSidebar" class="user-sidebar">
<div class="user-sidebar-header">
<span class="user-sidebar-title">Connected Users</span>
<button id="exitSidebarBtn" class="btn btn-exit-sidebar" title="Close sidebar">
<i class="fas fa-times"></i>
</button>
</div>
<div class="user-search-bar">
<input type="text" id="userSearchInput" placeholder="Search users..." autocomplete="off" />
</div>
<ul id="userList" class="user-list"></ul>
</div>
<!-- Sign-in Page -->
<div id="signInPage" class="container text-center center">
<div class="container mt-5">
@@ -102,10 +116,6 @@
<div class="row text-center">
<div class="col-md-12">
<!-- Input field for selecting the username to call -->
<select id="callUsernameSelect">
<option value="" disabled selected>Select a user to call</option>
</select>
<!-- Button to hide/show the local video -->
<button
id="hideBtn"
@@ -166,6 +176,17 @@
>
<i class="fas fa-phone-slash"></i>
</button>
<!-- Toggle user sidebar button -->
<button
id="sidebarBtn"
class="btn btn-custom btn-primary btn-m"
data-toggle="tooltip"
data-placement="top"
title="Show users"
>
<i class="fas fa-users"></i>
</button>
</div>
</div>
</div>
+118
View File
@@ -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;