First pass at adding a waiting room.
This commit is contained in:
+29
-3
@@ -201,6 +201,10 @@ function ServerConnection() {
|
||||
* @type{(this: ServerConnection, kind: string, hall: string, permissions: Array<string>, status: Object<string,any>, data: Object<string,any>, error: string, message: string) => void}
|
||||
*/
|
||||
this.onjoined = null;
|
||||
/** Called for authenticated waiting-room state changes. */
|
||||
this.onwaiting = null;
|
||||
/** Called for operator-only waiting-list changes. */
|
||||
this.onwaitinglist = null;
|
||||
/**
|
||||
* ondownstream is called whenever a new down stream is added. It
|
||||
* should set up the stream's callbacks; actually setting up the UI
|
||||
@@ -342,7 +346,7 @@ ServerConnection.prototype.connect = function(url) {
|
||||
try {
|
||||
sc.send({
|
||||
type: 'handshake',
|
||||
version: ['2'],
|
||||
version: ['3'],
|
||||
id: sc.id,
|
||||
});
|
||||
} catch(e) {
|
||||
@@ -391,8 +395,8 @@ ServerConnection.prototype.connect = function(url) {
|
||||
sc.lastServerMessage = new Date().valueOf();
|
||||
switch(m.type) {
|
||||
case 'handshake': {
|
||||
if((m.version instanceof Array) && m.version.includes('2')) {
|
||||
sc.version = '2';
|
||||
if((m.version instanceof Array) && m.version.includes('3')) {
|
||||
sc.version = '3';
|
||||
} else {
|
||||
sc.version = null;
|
||||
sc.error(new Error(`Unknown protocol version ${m.version}`));
|
||||
@@ -449,6 +453,21 @@ ServerConnection.prototype.connect = function(url) {
|
||||
m.status, m.data,
|
||||
m.error || null, m.value || null);
|
||||
break;
|
||||
case 'waiting':
|
||||
if(m.kind === 'enter' && m.value) {
|
||||
sc.username = m.value.username;
|
||||
sc.permissions = [];
|
||||
} else if(m.kind === 'leave' || m.kind === 'cancel') {
|
||||
sc.username = null;
|
||||
sc.permissions = [];
|
||||
}
|
||||
if(sc.onwaiting)
|
||||
sc.onwaiting.call(sc, m.kind, m.value || null);
|
||||
break;
|
||||
case 'waiting-list':
|
||||
if(sc.onwaitinglist)
|
||||
sc.onwaitinglist.call(sc, m.kind, m.value || null);
|
||||
break;
|
||||
case 'user':
|
||||
let user = null;
|
||||
switch(m.kind) {
|
||||
@@ -651,6 +670,13 @@ ServerConnection.prototype.leave = function(hall) {
|
||||
});
|
||||
};
|
||||
|
||||
ServerConnection.prototype.waitingAction = function(kind, value) {
|
||||
let message = {type: 'waiting', kind: kind};
|
||||
if(typeof value !== 'undefined')
|
||||
message.value = value;
|
||||
this.send(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* request sets the list of requested tracks
|
||||
*
|
||||
|
||||
+26
-1
@@ -982,6 +982,8 @@ legend {
|
||||
#left-sidebar {
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #dcdcdc;
|
||||
@@ -1035,7 +1037,8 @@ header .collapse:hover {
|
||||
#users {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: calc(100% - 84px);
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -1044,6 +1047,28 @@ header .collapse:hover {
|
||||
border: 1px solid #f7f7f7;
|
||||
}
|
||||
|
||||
#waiting-list-panel {
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid #ddd;
|
||||
background: #fff;
|
||||
max-height: 40%;
|
||||
overflow-y: auto;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
#waiting-list-panel h2 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.waiting-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin: 0.4rem 0;
|
||||
}
|
||||
|
||||
#users .user-p {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
</div>
|
||||
<div class="header-sep"></div>
|
||||
<div id="users"></div>
|
||||
<section id="waiting-list-panel" class="invisible" aria-labelledby="waiting-list-heading">
|
||||
<h2 id="waiting-list-heading">Waiting room (0)</h2>
|
||||
<div id="waiting-list"></div>
|
||||
</section>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<header class="connected-only invisible" aria-hidden="true">
|
||||
@@ -105,6 +109,18 @@
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
<section id="waiting-room" class="login-container invisible" aria-labelledby="waiting-heading">
|
||||
<div class="login-box">
|
||||
<h1 id="waiting-heading" tabindex="-1">Waiting room</h1>
|
||||
<p id="waiting-status"></p>
|
||||
<p>
|
||||
<input id="waiting-auto-join" type="checkbox"/>
|
||||
<label for="waiting-auto-join">Join hall automatically when it unlocks</label>
|
||||
</p>
|
||||
<p><button id="waiting-join" type="button" class="btn btn-blue" disabled>Join hall</button></p>
|
||||
<p><button id="waiting-leave" type="button" class="btn btn-default">Leave waiting room</button></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+158
-1
@@ -71,6 +71,10 @@ let token = null;
|
||||
*/
|
||||
let probingState = null;
|
||||
|
||||
let waitingState = null;
|
||||
let waitingCountdownTimer = null;
|
||||
let waitingUsers = new Map();
|
||||
|
||||
/**
|
||||
* @typedef {Object} settings - the type of stored settings
|
||||
* @property {boolean} [localMute]
|
||||
@@ -516,12 +520,138 @@ function setConnected(connected) {
|
||||
} else {
|
||||
closeNav();
|
||||
userbox.classList.add('invisible');
|
||||
connectionbox.classList.remove('invisible');
|
||||
if(!waitingState)
|
||||
connectionbox.classList.remove('invisible');
|
||||
;
|
||||
window.onresize = null;
|
||||
}
|
||||
}
|
||||
|
||||
function waitingHallName(state = waitingState) {
|
||||
return state && state.displayName ? state.displayName :
|
||||
(hallStatus.displayName || capitalise(hall));
|
||||
}
|
||||
|
||||
function updateWaitingStatus(announceSchedule = false) {
|
||||
if(!waitingState)
|
||||
return;
|
||||
let name = waitingHallName();
|
||||
let status;
|
||||
if(!waitingState.locked) {
|
||||
status = `The ${name} hall is open. You may join now.`;
|
||||
} else if(waitingState.deadline) {
|
||||
let remaining = Math.max(1, Math.ceil(
|
||||
(new Date(waitingState.deadline).getTime() - Date.now()) / 60000,
|
||||
));
|
||||
status = `The ${name} hall is locked. It is scheduled to unlock in ${remaining} minute${remaining === 1 ? '' : 's'}.`;
|
||||
} else {
|
||||
status = `The ${name} hall is locked. An operator is present; no unlock time is set.`;
|
||||
}
|
||||
document.getElementById('waiting-status').textContent = status;
|
||||
getButtonElement('waiting-join').disabled = !!waitingState.locked;
|
||||
if(announceSchedule)
|
||||
announceChat(status);
|
||||
}
|
||||
|
||||
function showWaitingRoom(state) {
|
||||
waitingState = state;
|
||||
token = null;
|
||||
getInputElement('waiting-auto-join').checked = false;
|
||||
document.getElementById('login-container').classList.add('invisible');
|
||||
document.getElementById('waiting-room').classList.remove('invisible');
|
||||
document.getElementById('waiting-heading').textContent =
|
||||
`Waiting for the ${waitingHallName(state)} hall`;
|
||||
updateWaitingStatus(false);
|
||||
if(waitingCountdownTimer)
|
||||
clearInterval(waitingCountdownTimer);
|
||||
waitingCountdownTimer = setInterval(() => updateWaitingStatus(false), 30000);
|
||||
document.getElementById('waiting-heading').focus({preventScroll: true});
|
||||
announceChat(`You are in the waiting room for the ${waitingHallName(state)} hall.`);
|
||||
}
|
||||
|
||||
function hideWaitingRoom() {
|
||||
if(waitingCountdownTimer) {
|
||||
clearInterval(waitingCountdownTimer);
|
||||
waitingCountdownTimer = null;
|
||||
}
|
||||
waitingState = null;
|
||||
document.getElementById('waiting-room').classList.add('invisible');
|
||||
}
|
||||
|
||||
function gotWaiting(kind, state) {
|
||||
switch(kind) {
|
||||
case 'enter':
|
||||
showWaitingRoom(state);
|
||||
break;
|
||||
case 'update':
|
||||
case 'open': {
|
||||
let oldDeadline = waitingState && waitingState.deadline;
|
||||
let wasLocked = waitingState && waitingState.locked;
|
||||
waitingState = state;
|
||||
updateWaitingStatus(oldDeadline !== state.deadline || wasLocked !== state.locked);
|
||||
break;
|
||||
}
|
||||
case 'approved':
|
||||
waitingState = state;
|
||||
announceChat(`An operator admitted you to the ${waitingHallName(state)} hall.`);
|
||||
break;
|
||||
case 'cancel':
|
||||
hideWaitingRoom();
|
||||
announceUrgent(state || 'Waiting ended.');
|
||||
this.close();
|
||||
break;
|
||||
case 'leave':
|
||||
hideWaitingRoom();
|
||||
this.close();
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown waiting-room event ${kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWaitingList() {
|
||||
let panel = document.getElementById('waiting-list-panel');
|
||||
let list = document.getElementById('waiting-list');
|
||||
let isOperator = serverConnection &&
|
||||
serverConnection.permissions.indexOf('op') >= 0;
|
||||
let visible = isOperator && !!hallStatus.locked;
|
||||
panel.classList.toggle('invisible', !visible);
|
||||
document.getElementById('waiting-list-heading').textContent =
|
||||
`Waiting room (${waitingUsers.size})`;
|
||||
list.replaceChildren();
|
||||
if(!visible)
|
||||
return;
|
||||
for(let user of waitingUsers.values()) {
|
||||
let row = document.createElement('div');
|
||||
row.className = 'waiting-user';
|
||||
let name = document.createElement('span');
|
||||
name.textContent = user.username + (user.approved ? ' (admission approved)' : '');
|
||||
let button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.textContent = user.approved ? 'Approved' : 'Admit';
|
||||
button.disabled = !!user.approved;
|
||||
button.setAttribute('aria-label', `Admit ${user.username}`);
|
||||
button.onclick = () => serverConnection.waitingAction('admit', user.username);
|
||||
row.append(name, button);
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function gotWaitingList(kind, value) {
|
||||
if(kind === 'snapshot') {
|
||||
waitingUsers.clear();
|
||||
for(let user of value || [])
|
||||
waitingUsers.set(user.username, user);
|
||||
} else if(kind === 'add' && value) {
|
||||
waitingUsers.set(value.username, value);
|
||||
announceChat(`${value.username} entered the waiting room.`);
|
||||
} else if(kind === 'remove' && value) {
|
||||
waitingUsers.delete(value.username);
|
||||
announceChat(`${value.username} left the waiting room.`);
|
||||
}
|
||||
renderWaitingList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we connect to the server.
|
||||
*
|
||||
@@ -635,6 +765,7 @@ function onPeerConnection() {
|
||||
function gotClose(code, reason) {
|
||||
closeUpMedia();
|
||||
closeSafariStream();
|
||||
hideWaitingRoom();
|
||||
setConnected(false);
|
||||
resetChalkboard();
|
||||
if(code != 1000) {
|
||||
@@ -2211,6 +2342,9 @@ async function gotJoined(kind, hall, perms, status, data, error, message) {
|
||||
} else {
|
||||
token = null;
|
||||
}
|
||||
if(kind === 'join' && waitingState)
|
||||
announceChat(`Joining the ${waitingHallName()} hall.`);
|
||||
hideWaitingRoom();
|
||||
// don't discard endPoint and friends
|
||||
hallStatus.locked = false;
|
||||
hallStatus.recording = false;
|
||||
@@ -2225,6 +2359,7 @@ async function gotJoined(kind, hall, perms, status, data, error, message) {
|
||||
setChangePassword(pwAuth && !!hallStatus.canChangePassword &&
|
||||
serverConnection.username
|
||||
);
|
||||
renderWaitingList();
|
||||
openSafariStream();
|
||||
window.setTimeout(() => {
|
||||
userNotificationSoundsReady = true;
|
||||
@@ -2273,6 +2408,14 @@ async function gotJoined(kind, hall, perms, status, data, error, message) {
|
||||
);
|
||||
}
|
||||
}
|
||||
let displayName = hallStatus.displayName || capitalise(hall);
|
||||
let microphone = findUpMedia('audio');
|
||||
if(!microphone) {
|
||||
announceChat(`Joined the ${displayName} hall. Your microphone is not enabled.`);
|
||||
} else {
|
||||
let enabled = microphone.stream && microphone.stream.getAudioTracks().some(track => track.enabled);
|
||||
announceChat(`Joined the ${displayName} hall. Your microphone is ${enabled ? 'unmuted' : 'muted'}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4069,6 +4212,18 @@ document.getElementById('disconnectbutton').onclick = function(e) {
|
||||
closeNav();
|
||||
};
|
||||
|
||||
getInputElement('waiting-auto-join').onchange = function() {
|
||||
serverConnection.waitingAction('auto', this.checked);
|
||||
};
|
||||
|
||||
getButtonElement('waiting-join').onclick = function() {
|
||||
serverConnection.waitingAction('join');
|
||||
};
|
||||
|
||||
getButtonElement('waiting-leave').onclick = function() {
|
||||
serverConnection.waitingAction('leave');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {boolean} hidden
|
||||
@@ -4186,6 +4341,8 @@ async function serverConnect() {
|
||||
serverConnection.ondownstream = gotDownStream;
|
||||
serverConnection.onuser = gotUser;
|
||||
serverConnection.onjoined = gotJoined;
|
||||
serverConnection.onwaiting = gotWaiting;
|
||||
serverConnection.onwaitinglist = gotWaitingList;
|
||||
serverConnection.onchat = addToChatbox;
|
||||
serverConnection.onusermessage = gotUserMessage;
|
||||
serverConnection.onfiletransfer = gotFileTransfer;
|
||||
|
||||
Reference in New Issue
Block a user