[call-me] #8 - add Searchable user list
This commit is contained in:
Generated
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user