From cb61c429625c6e63e420c9ec3308c3d17786af0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20D=C3=B6ring?= <simon.doering@stud.hs-bochum.de> Date: Fri, 15 Jan 2021 22:28:59 +0100 Subject: [PATCH] Finish sender UI and implement name change --- .../src/socket-io/handlers/sender-handlers.ts | 125 +++++++--- camera-server/src/util/escape-html.ts | 8 + sender/camera-sender.css | 181 +++++++++++--- sender/camera-sender.html | 47 ++-- sender/camera-sender.js | 228 ++++++++++++------ 5 files changed, 428 insertions(+), 161 deletions(-) create mode 100644 camera-server/src/util/escape-html.ts diff --git a/camera-server/src/socket-io/handlers/sender-handlers.ts b/camera-server/src/socket-io/handlers/sender-handlers.ts index 5fbcc69..2c18aba 100644 --- a/camera-server/src/socket-io/handlers/sender-handlers.ts +++ b/camera-server/src/socket-io/handlers/sender-handlers.ts @@ -3,6 +3,7 @@ import { emitNewFeed, emitRemoveFeed } from './common-handlers'; import { SenderSocket } from '../../models/sender-socket'; import { ValidationError } from '../../models/validation-error'; import { notifyCustomName } from '../../io-interface/handlers/output-handlers'; +import { escapeHTML } from '../../util/escape-html'; const handleSetFeedId = ( socket: SenderSocket, @@ -57,24 +58,26 @@ const handleSetFeedId = ( throw new ValidationError('No feed id was provided'); } - const unescapedCustomName = data.customName; + let unescapedCustomName = data.customName; if (unescapedCustomName != null) { - console.log( - `Got custom name from slot ${slot}: ${unescapedCustomName}` - ); - const customName = unescapedCustomName - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - if (unescapedCustomName !== customName) { + unescapedCustomName = unescapedCustomName.trim(); + if (unescapedCustomName.length > 0) { + console.log( + `Got custom name from slot ${slot}: ${unescapedCustomName}` + ); + const customName = escapeHTML(unescapedCustomName); + if (unescapedCustomName !== customName) { + console.log( + `Warning: Escaped custom name (${customName}) does not equal the unescaped custom name` + + `(${unescapedCustomName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack` + ); + } + notifyCustomName(slot, customName); + } else { console.log( - `Warning: Escaped custom name (${customName}) does not equal the unescaped custom name` + - `(${unescapedCustomName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack` + 'Error: Got a name that is either empty or consists only of whitespaces' ); } - notifyCustomName(slot, customName); } console.log('Setting feed id of slot ' + slot + ' to ' + feedId); @@ -96,29 +99,83 @@ const handleSetFeedId = ( fn({ success, message }); }; - -const handleSenderDisconnect = (socket: SenderSocket, _: string) => { +const handleChangeName = ( + socket: SenderSocket, + data: null | { newName?: string } +) => { const slot = socket.cameraSlot; - if (slot != null) { - const currentSlotState = cameraSlotState[slot]; - if ( - currentSlotState.feedActive && - socket.id === currentSlotState.senderSocketId - ) { + const currentSlotState = cameraSlotState[slot]; + + if (!currentSlotState.feedActive) { + console.log( + 'Error: Got change_name event on slot ' + + slot + + ' which has no active feed' + ); + return; + } + + if (socket.id !== currentSlotState.senderSocketId) { + console.log( + 'Error: Got change_name event on slot ' + + slot + + ' from somebody who is not the sender' + ); + return; + } + + if (data == null) { + console.log( + 'Error: Got change_name event with no data on slot ' + slot + ); + return; + } + + let unescapedNewName = data.newName; + if (unescapedNewName == null) { + console.log( + 'Error: Got change_name event with no new name on slot' + slot + ); + return; + } + + unescapedNewName = unescapedNewName.trim(); + if (unescapedNewName.length > 0) { + console.log(`Got new name for slot ${slot}: ${unescapedNewName}`); + const newName = escapeHTML(unescapedNewName); + if (unescapedNewName !== newName) { console.log( - 'Sender on slot ' + slot + ' disconnected - Clearing slot' + `Warning: Escaped new name (${newName}) does not equal the unescaped new name` + + `(${unescapedNewName}) on slot ${slot} - this could mean that the user tried a Cross-Site-Scripting (XSS) attack` ); - currentSlotState.feedActive = false; - currentSlotState.feedId = null; - currentSlotState.senderSocketId = null; - - emitRemoveFeed(slot); } + notifyCustomName(slot, newName); + } else { + console.log( + 'Error: Got a name that is either empty or consists only of whitespaces' + ); + } +}; + +const handleSenderDisconnect = (socket: SenderSocket, _: string) => { + const slot = socket.cameraSlot; + const currentSlotState = cameraSlotState[slot]; + if ( + currentSlotState.feedActive && + socket.id === currentSlotState.senderSocketId + ) { + console.log('Sender on slot ' + slot + ' disconnected - Clearing slot'); + currentSlotState.feedActive = false; + currentSlotState.feedId = null; + currentSlotState.senderSocketId = null; + + emitRemoveFeed(slot); } }; const registerSenderHandlers = (socket: SenderSocket) => { socket.on('set_feed_id', handleSetFeedId.bind(null, socket)); + socket.on('change_name', handleChangeName.bind(null, socket)); socket.on('disconnect', handleSenderDisconnect.bind(null, socket)); }; @@ -149,7 +206,7 @@ export const handleSenderInit = ( ' that cannot be parsed to a number' ); throw new ValidationError( - 'Slot ' + slotStr + ' cannot be parsed to number' + 'An invalid camera slot was provided (' + slotStr + ')' ); } if (slot < 0 || slot > cameraSlotState.length - 1) { @@ -158,9 +215,7 @@ export const handleSenderInit = ( slot + ' which is not in the list of slots' ); - throw new ValidationError( - 'Slot ' + slot + ' is not in the list of slots' - ); + throw new ValidationError('This camera slot does not exist'); } const slotState = cameraSlotState[slot]; @@ -168,7 +223,9 @@ export const handleSenderInit = ( console.log( 'Error: Got socket connection for inactive slot ' + slot ); - throw new ValidationError('Slot ' + slot + ' is not active'); + throw new ValidationError( + 'This camera slot is not activated, contact your moderator' + ); } const token = data.token; @@ -183,7 +240,7 @@ export const handleSenderInit = ( ' for slot ' + slot ); - throw new ValidationError('Invalid token'); + throw new ValidationError('Invalid token, contact your moderator'); } console.log('Got sender socket connection on slot ' + slot); diff --git a/camera-server/src/util/escape-html.ts b/camera-server/src/util/escape-html.ts new file mode 100644 index 0000000..5905bce --- /dev/null +++ b/camera-server/src/util/escape-html.ts @@ -0,0 +1,8 @@ +export const escapeHTML = (raw: string) => { + return raw + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}; diff --git a/sender/camera-sender.css b/sender/camera-sender.css index f69b6f2..04653ed 100644 --- a/sender/camera-sender.css +++ b/sender/camera-sender.css @@ -4,6 +4,19 @@ box-sizing: border-box; } +:root { + --green-1: #23fb64; + --green-2: #05f04b; + --green-3: #04b439; + --red-1: #e75f68; + --red-2: #e23c47; + --yellow-1: #e47e11; + --light-blue-1: #cde0e6; + --light-blue-2: #91d9f1; + --light-blue-3: #75bfd9; + --background-color: #eee; +} + body { font-size: 16px; margin: 0; @@ -18,10 +31,6 @@ body { } } -.visually-hidden { - visibility: hidden; -} - .main__seperator { margin: 16px 0; } @@ -33,6 +42,7 @@ body { padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); border-radius: 8px; + background: var(--background-color); } .main__header { @@ -40,13 +50,26 @@ body { } .main__header-title { + margin: 0 0 8px; +} + +.main__header-subtitle { margin: 0; } -#connection-status { - margin-bottom: 8px; +#status { + margin: 0 auto 8px; + max-width: 80%; text-align: center; - color: green; + color: var(--green-3); +} + +#status[data-status-code="1"] { + color: var(--yellow-1); +} + +#status[data-status-code="2"] { + color: var(--red-2); } .spinner-wrapper { @@ -62,15 +85,11 @@ body { height: 30px; border-radius: 50%; border: solid transparent 2px; - --border-color: #00bfff; + --border-color: var(--light-blue-3); border-top-color: var(--border-color); border-bottom-color: var(--border-color); } -.main__room-form { - margin: 0; -} - .form-control { margin-bottom: 8px; display: flex; @@ -80,49 +99,90 @@ body { } .form-control__label { - width: 120px; + width: 150px; padding-right: 4px; } .form-control__input { - min-width: 100px; - width: 100px; - max-width: 200px; - flex-grow: 1; + min-width: 150px; + width: 150px; + max-width: 250px; + flex: 1; } -#preview-container { - height: 100px; - background: orange; +.name-control__form { display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16px; + margin: 0; } -.main__options { - margin-bottom: 8px; +.name-control__input { + flex: 1 0 0; + min-width: 0; } -.options__toggle-wrapper { - display: flex; - justify-content: center; - margin-bottom: 6px; +.name-control__change { + margin-left: 4px; +} + +#preview-container { + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + margin: 12px 0; + overflow: hidden; +} + +#camera-preview { + width: 100%; +} + +.main__options { + margin-bottom: 8px; } .options__toggle { + border: none; + outline: none; + width: 100%; + font-size: 14px; + position: relative; text-align: center; - padding: 4px 8px; - border-radius: 4px; - background: #eee; + z-index: 2; + background: var(--light-blue-2); + border-radius: 8px; + padding: 8px; } .options__toggle:hover { - background: #ccc; + background: var(--light-blue-3); } -.options__body--hidden { - display: none; +.options__body { + border-radius: 0 0 8px 8px; + background: var(--light-blue-1); + margin-top: -8px; + padding: 16px 8px 8px; +} + +.options__hint { + margin: 0 0 4px; +} + +#bandwidth-form { + margin-bottom: 0; +} + +.bandwidth-form__control { + display: flex; +} + +.bandwidth-form__input { + flex: 2 0 50px; + min-width: 0; + margin-right: 4px; +} + +.bandwidth-form__button { + flex: 1 0 0; } .main__controls { @@ -133,7 +193,54 @@ body { .main__control-button { width: 100%; - max-width: 200px; margin: 0 4px; - + outline: none; + border: none; + border-radius: 4px; + padding: 8px 0; +} + +.main__control-button:disabled { + filter: grayscale(0.6); +} + +#start { + background: var(--green-1); +} + +#start:not(:disabled):hover { + background: var(--green-2); +} + +#stop { + background: var(--red-1); +} + +#stop:not(:disabled):hover { + background: var(--red-2); +} + +#reload { + background: var(--light-blue-2); +} + +#reload:hover { + background: var(--light-blue-3); +} + +.hidden { + display: none; +} + +@media (max-width: 640px) { + body { + background: var(--background-color); + } + + .main { + box-shadow: none; + border-radius: 0; + width: 100%; + margin: 8px auto; + } } diff --git a/sender/camera-sender.html b/sender/camera-sender.html index 28cb555..54f4af9 100644 --- a/sender/camera-sender.html +++ b/sender/camera-sender.html @@ -2,6 +2,7 @@ <head> <title>Camera Sender</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta charset="utf-8"> <script type="text/javascript" src="adapter.min.js"></script> <script type="text/javascript" src="janus.js"></script> <script type="text/javascript" src="socket.io.min.js"></script> @@ -11,20 +12,21 @@ <body> <main class="main"> <header class="main__header"> - <h3 id="room-indicator" class="main__header-title"> + <h2 class="main__header-title">CVH-Camera</h2> + <h3 id="room-indicator" class="main__header-subtitle"> Channel <span id="room"></span> • Camera <span id="slot"></span> </h3> </header> <hr class="main__seperator" /> - <div id="connection-status">Connected</div> + <div id="status">Connected</div> <div id="spinner" class="spinner-wrapper"> <div class="spinner"></div> </div> - <form id="room-form" class="main__room-form"> + <div id="inputs-container" class="main__inputs-container hidden"> <div class="form-control"> <label class="form-control__label" for="res-select">Resolution</label> <select id="res-select" class="form-control__input"> @@ -37,29 +39,31 @@ </select> </div> - <div id="name-control" class="form-control"> - <label class="form-control__label" for="name-input">Display name</label> - <input id="name-input" class="form-control__input" required type="text" /> - </div> - - <div id="pin-control" class="form-control"> + <div id="pin-control" class="form-control hidden"> <label class="form-control__label" for="pin-input">Room PIN</label> <input id="pin-input" class="form-control__input" type="password" placeholder="Leave empty if none" /> </div> - </form> - <div id="preview-container">Preview</div> - - <section class="main__options"> - <div class="options__toggle-wrapper"> - <div id="options-toggle" class="options__toggle">Advanced options</div> + <div id="name-control" class="form-control hidden"> + <label class="form-control__label" for="name-input">Display name</label> + <form id="name-form" class="form-control__input name-control__form"> + <input id="name-input" class="name-control__input" required type="text" /> + <button disabled title="Change name" id="name-change" class="name-control__change hidden">></button> + </form> </div> - <div id="options" class="options__body options__body--hidden"> + </div> + + <div id="preview-container" class="hidden"></div> + + <section id="options-container" class="main__options hidden"> + <button id="options-toggle" class="options__toggle">Advanced options</button> + <div id="options" class="options__body hidden"> <form id="bandwidth-form"> - <label>While the camera is running, a bandwidth cap can be set here. 0 or negative means no cap.</label> - <br /> - <input type="text" id="bandwidth-input" placeholder="New bitrate [in kbit/s]" /> - <input type="submit" id="bandwidth-submit" value="Change" disabled /> + <p class="options__hint">While the camera is running, a bandwidth cap can be set here. 0 or negative means no cap.</p> + <div class="bandwidth-form__control"> + <input type="text" placeholder="Bitrate [in kbit/s]" id="bandwidth-input" class="bandwidth-form__input" /> + <button type="submit" id="bandwidth-submit" class="bandwidth-form__button" disabled>Change</button> + </div> </form> </div> </section> @@ -67,8 +71,9 @@ <hr class="main__seperator" /> <div class="main__controls"> - <button id="start" class="main__control-button" type="submit" disabled>Start</button> + <button id="start" class="main__control-button" disabled>Start</button> <button id="stop" class="main__control-button" disabled>Stop</button> + <button id="reload" class="main__control-button hidden">Reload</button> </div> </main> </body> diff --git a/sender/camera-sender.js b/sender/camera-sender.js index ec29de9..f74a911 100644 --- a/sender/camera-sender.js +++ b/sender/camera-sender.js @@ -5,7 +5,6 @@ document.addEventListener('DOMContentLoaded', function() { var videoroomHandle = null; var sendResolution = 'stdres'; - var roomForm = document.getElementById('room-form'); var startButton = document.getElementById('start'); var stopButton = document.getElementById('stop'); @@ -19,6 +18,12 @@ document.addEventListener('DOMContentLoaded', function() { var customNameAllowed = false; var feedId = null; + var STATUS_CODE = { + success: 0, + warning: 1, + error: 2 + }; + parseRoomFromURL(); parseSlotFromURL(); parsePinFromURL(); @@ -32,13 +37,38 @@ document.addEventListener('DOMContentLoaded', function() { const socket = io('https://' + window.location.hostname, { path: '/socket.io/' + socketNumber }); + setStatusMessage('Connecting to camera server...'); - document.getElementById('options-toggle').onclick = function () { - document.getElementById('options').classList.toggle('options__body--hidden'); + document.getElementById('options-toggle').onclick = function() { + document.getElementById('options').classList.toggle('hidden'); }; + document.getElementById('reload').onclick = function() { + window.location.reload(); + }; + + var timeoutTime = 10000; + var cameraServerTimeout = setTimeout(function() { + setStatusMessage('Camera server connection timeout... Please try again later', STATUS_CODE.error); + }, timeoutTime); + registerSocketHandlers(); + function registerSocketHandlers() { + socket.on('connect', function() { + clearTimeout(cameraServerTimeout); + // This event will be triggered on every connect including reconnects + // That's why the check is necessary to ensure that the event is only emitted once + if (!gotSocketInitResponse) { + socket.emit( + 'sender_init', + { slot, token }, + handleSenderInitResponse + ); + } + }); + }; + function handleSenderInitResponse(data) { if (!gotSocketInitResponse) { gotSocketInitResponse = true; @@ -46,7 +76,7 @@ document.addEventListener('DOMContentLoaded', function() { if (data.success) { initJanus(); } else { - alert('Socket connection error:\n' + data.message); + setStatusMessage(`Socket connection error: ${data.message} - Reload to try again`, STATUS_CODE.error); } } } @@ -62,40 +92,36 @@ document.addEventListener('DOMContentLoaded', function() { bandwidthSubmit.removeAttribute('disabled', ''); stopButton.removeAttribute('disabled'); stopButton.onclick = function() { - janus.destroy(); + setStatusMessage('Transmission stopped', STATUS_CODE.warning); + cleanup(); }; - var video = document.getElementById('camera-preview'); - if (video != null) { - video.classList.remove('visually-hidden'); + showVideo(); + showOptions(); + if (customNameAllowed) { + var nameForm = document.getElementById('name-form'); + nameForm.onsubmit = function(event) { + event.preventDefault(); + const newName = document.getElementById('name-input').value; + socket.emit('change_name', { newName }); + setStatusMessage(`Requested name change to: ${newName}`); + }; + var nameChangeButton = document.getElementById('name-change'); + nameChangeButton.removeAttribute('disabled'); + showElement(nameChangeButton); } - alert('Sharing camera'); + setStatusMessage('Sharing camera'); } else { - alert('Error: ' + data.message); - window.location.reload(); + setStatusMessage(`Error: ${data.message} - Reload to try again`, STATUS_CODE.error); } } - function registerSocketHandlers() { - socket.on('connect', function() { - // This event will be triggered on every connect including reconnects - // That's why the check is necessary to ensure that the event is only emitted once - console.log('socket on connect handler'); - if (!gotSocketInitResponse) { - socket.emit( - 'sender_init', - { slot, token }, - handleSenderInitResponse - ); - } - }); - }; - function initJanus() { + setStatusMessage('Initializing Janus...') Janus.init({ debug: 'all', callback: function() { if (!Janus.isWebrtcSupported()) { - alert('No WebRTC support... '); + setStatusMessage('Your browser does not support camera transmission', STATUS_CODE.error); return; } @@ -108,23 +134,29 @@ document.addEventListener('DOMContentLoaded', function() { videoroomHandle = pluginHandle; Janus.log('Plugin attached! (' + videoroomHandle.getPlugin() + ', id=' + videoroomHandle.getId() + ')'); - roomForm.onsubmit = function(event) { - event.preventDefault(); + hideSpinner(); + showInputs(); + + startButton.onclick = function() { + setStatusMessage('Connecting...'); var resSelect = document.getElementById('res-select'); startButton.setAttribute('disabled', ''); resSelect.setAttribute('disabled', ''); sendResolution = resSelect.value; Janus.log('sendResolution:', sendResolution); if (useUserPin) { - pin = document.getElementById('pin-input').value; + var pinInputEl = document.getElementById('pin-input'); + pin = pinInputEl.value; + pinInputEl.setAttribute('disabled', ''); } shareCamera(pin); }; startButton.removeAttribute('disabled'); + setStatusMessage('Connected - Click Start to transmit your camera feed'); }, error: function(error) { Janus.error('Error attaching plugin: ', error); - alert(error); + setStatusMessage(`Janus attach error: ${error} - Reload to try again`, STATUS_CODE.error); }, webrtcState: function(on) { if (on) { @@ -146,9 +178,6 @@ document.addEventListener('DOMContentLoaded', function() { video.setAttribute('autoplay', ''); video.setAttribute('playsinline', ''); video.setAttribute('muted', 'muted'); - if (!transmitting) { - video.classList.add('visually-hidden'); - } document.getElementById('preview-container').appendChild(video); } Janus.attachMediaStream(document.getElementById('camera-preview'), stream); @@ -157,12 +186,10 @@ document.addEventListener('DOMContentLoaded', function() { }, error: function(error) { Janus.error(error); - alert(error); - window.location.reload(); + setStatusMessage(`Janus error: ${error} - Reload to try again`, STATUS_CODE.error); }, destroyed: function() { - alert('Stopped'); - window.location.reload(); + console.log('Janus destroyed!'); } }); }}); @@ -202,35 +229,29 @@ document.addEventListener('DOMContentLoaded', function() { Janus.log('Joined event:', msg); feedId = msg.id; Janus.log('Successfully joined room ' + msg['room'] + ' with ID ' + feedId); - //if (msg['publishers'].length === 0) { - videoroomHandle.createOffer({ - media: { - videoSend: true, - video: sendResolution, - audioSend: false, - videoRecv: false - }, - success: function(jsep) { - var publish = { - request: 'configure', - audio: false, - video: true - }; - videoroomHandle.send({ message: publish, jsep }); - }, - error: function(error) { - Janus.error('WebRTC error:', error); - alert('WebRTC error: ' + error.message); - } - }); - //} else { - // alert('There is already somebody who is sharing his camera in this room!'); - // window.location.reload(); - //} + videoroomHandle.createOffer({ + media: { + videoSend: true, + video: sendResolution, + audioSend: false, + videoRecv: false + }, + success: function(jsep) { + var publish = { + request: 'configure', + audio: false, + video: true + }; + videoroomHandle.send({ message: publish, jsep }); + }, + error: function(error) { + Janus.error('WebRTC error:', error); + setStatusMessage(`Janus WebRTC error: ${error.message} - Reload to try again`, STATUS_CODE.error); + } + }); } if (event === 'event' && msg['error']) { - alert('Error message: ' + msg['error'] + '.\nError object: ' + JSON.stringify(msg, null, 2)); - window.location.reload(); + setStatusMessage(`Janus error: ${msg['error']} - Reload to try again`, STATUS_CODE.error); } } if (jsep) { @@ -238,6 +259,76 @@ document.addEventListener('DOMContentLoaded', function() { } }; + function setStatusMessage(message, statusCode) { + // For error status messages a cleanup is performed automatically + // If this is not desired, use warnings + if (statusCode == null) { + statusCode = STATUS_CODE.success; + } + var statusEl = document.getElementById('status'); + statusEl.setAttribute('data-status-code', statusCode); + statusEl.innerText = message; + if (statusCode === STATUS_CODE.error) { + cleanup(); + } + } + + function cleanup() { + hideVideo(); + hideOptions(); + hideInputs(); + hideSpinner(); + showReload(); + if (videoroomHandle) { + videoroomHandle.detach(); + } + if (socket) { + socket.disconnect(); + } + } + + function hideElement(el) { + el.classList.add('hidden'); + } + + function showElement(el) { + el.classList.remove('hidden'); + } + + function hideSpinner() { + hideElement(document.getElementById('spinner')); + } + + function showInputs() { + showElement(document.getElementById('inputs-container')); + } + + function hideInputs() { + hideElement(document.getElementById('inputs-container')); + } + + function showOptions() { + showElement(document.getElementById('options-container')); + } + + function hideOptions() { + hideElement(document.getElementById('options-container')); + } + + function showVideo() { + showElement(document.getElementById('preview-container')); + } + + function hideVideo() { + hideElement(document.getElementById('preview-container')); + } + + function showReload() { + showElement(document.getElementById('reload')); + hideElement(startButton); + hideElement(stopButton); + } + function parseRoomFromURL() { var urlParams = new URLSearchParams(window.location.search); var roomParam = urlParams.get('room'); @@ -268,10 +359,9 @@ document.addEventListener('DOMContentLoaded', function() { if (pin === 'none') { pin = ''; } - document.getElementById('pin-container').remove(); - document.getElementById('pin-hint').remove(); } else { console.log('Got no valid pin in URL search params'); + showElement(document.getElementById('pin-control')); } } @@ -289,8 +379,8 @@ document.addEventListener('DOMContentLoaded', function() { var urlParams = new URLSearchParams(window.location.search); var param = urlParams.get('customNameAllowed'); customNameAllowed = param != null; - if (!customNameAllowed) { - document.getElementById('name-container').remove(); + if (customNameAllowed) { + showElement(document.getElementById('name-control')); } } }, false); -- GitLab