diff --git a/static/protocol.js b/static/protocol.js index d48349e..f212674 100644 --- a/static/protocol.js +++ b/static/protocol.js @@ -7,7 +7,8 @@ /** * toHex formats an array as a hexadecimal string. - * @returns {string} + * @param {number[]|Uint8Array} array - the array to format + * @returns {string} - the hexadecimal representation of array */ function toHex(array) { let a = new Uint8Array(array); @@ -37,43 +38,52 @@ function randomid() { function ServerConnection() { /** * The id of this connection. + * * @type {string} + * @const */ this.id = randomid(); /** * The group that we have joined, or nil if we haven't joined yet. + * * @type {string} */ this.group = null; /** * The underlying websocket. + * * @type {WebSocket} */ this.socket = null; /** * The set of all up streams, indexed by their id. - * @type {Object.} + * + * @type {Object} */ this.up = {}; /** * The set of all down streams, indexed by their id. - * @type {Object.} + * + * @type {Object} */ this.down = {}; /** * The ICE configuration used by all associated streams. - * @type {Array.} + * + * @type {RTCIceServer[]} */ this.iceServers = null; /** * The permissions granted to this connection. - * @type {Object.} + * + * @type {Object} */ this.permissions = {}; /** * clientdata is a convenient place to attach data to a ServerConnection. * It is not used by the library. - * @type{Object.} + * + * @type{Object} */ this.userdata = {}; @@ -81,46 +91,54 @@ function ServerConnection() { /** * onconnected is called when the connection has been established - * @type{function(): any} + * + * @type{(this: ServerConnection) => any} */ this.onconnected = null; /** * onclose is called when the connection is closed - * @type{function(number, string): any} + * + * @type{(this: ServerConnection, code: number, reason: string) => any} */ this.onclose = null; /** * onuser is called whenever a user is added or removed from the group - * @type{function(string, string, string): any} + * + * @type{(this: ServerConnection, id: string, kind: string, username: string) => any} */ this.onuser = null; /** * onpermissions is called whenever the current user's permissions change - * @type{function(Object.): any} + * + * @type{(this: ServerConnection, permissions: Object) => any} */ this.onpermissions = null; /** * ondownstream is called whenever a new down stream is added. It * should set up the stream's callbacks; actually setting up the UI * should be done in the stream's ondowntrack callback. - * @type{function(Stream): any} + * + * @type{(this: ServerConnection, stream: Stream) => any} */ this.ondownstream = null; /** * onchat is called whenever a new chat message is received. - * @type {function(string, string, string, string): any} + * + * @type {(this: ServerConnection, id: string, username: string, kind: string, message: string) => any} */ this.onchat = null; /** * onclearchat is called whenever the server requests that the chat * be cleared. - * @type{function(): any} + * + * @type{(this: ServerConnection) => any} */ this.onclearchat = null; /** * onusermessage is called when the server sends an error or warning * message that should be displayed to the user. - * @type{function(string, string): any} + * + * @type{(this: ServerConnection, kind: string, message: string) => any} */ this.onusermessage = null; } @@ -132,14 +150,14 @@ function ServerConnection() { * @property {string} [id] * @property {string} [username] * @property {string} [password] - * @property {Object.} [permissions] + * @property {Object} [permissions] * @property {string} [group] * @property {string} [value] * @property {RTCSessionDescriptionInit} [offer] * @property {RTCSessionDescriptionInit} [answer] * @property {RTCIceCandidate} [candidate] - * @property {Object.} [labels] - * @property {Object.} [request] + * @property {Object} [labels] + * @property {Object} [request] */ /** @@ -157,17 +175,18 @@ ServerConnection.prototype.close = function() { */ ServerConnection.prototype.send = function(m) { if(!this.socket || this.socket.readyState !== this.socket.OPEN) { - // send on a closed connection doesn't throw + // send on a closed socket doesn't throw throw(new Error('Connection is not open')); } return this.socket.send(JSON.stringify(m)); } -/** getIceServers fetches an ICE configuration from the server and +/** + * getIceServers fetches an ICE configuration from the server and * populates the iceServers field of a ServerConnection. It is called * lazily by connect. * - * @returns {Promise>} + * @returns {Promise} * @function */ ServerConnection.prototype.getIceServers = async function() { @@ -294,8 +313,8 @@ ServerConnection.prototype.connect = async function(url) { /** * login authenticates with the server. * - * @param {string} username - * @param {string} password + * @param {string} username - the username to login as. + * @param {string} password - the password. */ ServerConnection.prototype.login = function(username, password) { this.send({ @@ -324,7 +343,7 @@ ServerConnection.prototype.join = function(group) { * @param {string} what - One of '', 'audio', 'screenshare' or 'everything'. */ ServerConnection.prototype.request = function(what) { - /** @type {Object.} */ + /** @type {Object} */ let request = {}; switch(what) { case '': @@ -422,7 +441,7 @@ ServerConnection.prototype.chat = function(username, kind, message) { * * @param {string} kind - One of "clearchat", "lock", "unlock", "record or * "unrecord". - * @param {string} [message] + * @param {string} [message] - An optional user-readable message. */ ServerConnection.prototype.groupAction = function(kind, message) { this.send({ @@ -436,8 +455,8 @@ ServerConnection.prototype.groupAction = function(kind, message) { * userAction sends a request to act on a user. * * @param {string} kind - One of "op", "unop", "kick", "present", "unpresent". - * @param {string} id - * @param {string} [message] + * @param {string} id - The id of the user to act upon. + * @param {string} [message] - An optional user-readable message. */ ServerConnection.prototype.userAction = function(kind, id, message) { this.send({ @@ -452,7 +471,7 @@ ServerConnection.prototype.userAction = function(kind, id, message) { * Called when we receive an offer from the server. Don't call this. * * @param {string} id - * @param {Object.} labels + * @param {Object} labels * @param {RTCSessionDescriptionInit} offer * @param {boolean} renegotiate * @function @@ -656,13 +675,15 @@ function Stream(sc, id, pc) { * The associated ServerConnection. * * @type {ServerConnection} - */ + * @const + */ this.sc = sc; /** * The id of this stream. * * @type {string} - */ + * @const + */ this.id = id; /** * For up streams, one of "local" or "screenshare". @@ -693,20 +714,20 @@ function Stream(sc, id, pc) { /** * Track labels, indexed by track id. * - * @type {Object.} + * @type {Object} */ this.labels = {}; /** * Track labels, indexed by mid. * - * @type {Object.} + * @type {Object} */ this.labelsByMid = {}; /** * Buffered ICE candidates. This will be flushed by flushIceCandidates * when the PC becomes stable. * - * @type {Array.} + * @type {RTCIceCandidate[]} */ this.iceCandidates = []; /** @@ -721,7 +742,7 @@ function Stream(sc, id, pc) { * a dictionary indexed by track id, with each value a disctionary of * statistics. * - * @type {Object.} + * @type {Object} */ this.stats = {}; /** @@ -734,7 +755,7 @@ function Stream(sc, id, pc) { /** * clientdata is a convenient place to attach data to a Stream. * It is not used by the library. - * @type{Object.} + * @type{Object} */ this.userdata = {}; @@ -743,21 +764,21 @@ function Stream(sc, id, pc) { /** * onclose is called when the stream is closed. * - * @type{function(): any} + * @type{(this: Stream) => any} */ this.onclose = null; /** * onerror is called whenever an error occurs. If the error is * fatal, then onclose will be called afterwards. * - * @type{function(any): any} + * @type{(this: Stream, error: any) => any} */ this.onerror = null; /** * onnegotiationcompleted is called whenever negotiation or * renegotiation has completed. * - * @type{function(): any} + * @type{(this: Stream) => any} */ this.onnegotiationcompleted = null; /** @@ -765,32 +786,32 @@ function Stream(sc, id, pc) { * If the stream parameter differs from its previous value, then it * indicates that the old stream has been discarded. * - * @type{function(MediaStreamTrack, RTCRtpTransceiver, string, MediaStream): any} + * @type{(this: Stream, track: MediaStreamTrack, transceiver: RTCRtpTransceiver, label: string, stream: MediaStream) => any} */ this.ondowntrack = null; /** * onlabel is called whenever the server sets a new label for the stream. * - * @type{function(string): any} + * @type{(this: Stream, label: string) => any} */ this.onlabel = null; /** * onstatus is called whenever the status of the stream changes. * - * @type{function(string): any} + * @type{(this: Stream, status: string) => any} */ this.onstatus = null; /** * onabort is called when the server requested that an up stream be * closed. It is the resposibility of the client to close the stream. * - * @type{function(): any} + * @type{(this: Stream) => any} */ this.onabort = null; /** * onstats is called when we have new statistics about the connection * - * @type{function(Object.): any} + * @type{(this: Stream, stats: Object) => any} */ this.onstats = null; } @@ -834,6 +855,7 @@ Stream.prototype.close = function(sendclose) { * @function */ Stream.prototype.flushIceCandidates = async function () { + /** @type {Promise[]} */ let promises = []; this.iceCandidates.forEach(c => { promises.push(this.pc.addIceCandidate(c).catch(console.warn)); @@ -845,16 +867,16 @@ Stream.prototype.flushIceCandidates = async function () { /** * negotiate negotiates or renegotiates an up stream. It is called * automatically when required. If the client requires renegotiation, it - * is probably more effective to call restartIce on the underlying PC - * rather than invoking this function directly. + * is probably better to call restartIce which will cause negotiate to be + * called asynchronously. * * @function - * @param {boolean} [restartIce] + * @param {boolean} [restartIce] - Whether to restart ICE. */ Stream.prototype.negotiate = async function (restartIce) { let c = this; - let options = null; + let options = {}; if(restartIce) options = {iceRestart: true}; let offer = await c.pc.createOffer(options); @@ -892,8 +914,7 @@ Stream.prototype.negotiate = async function (restartIce) { Stream.prototype.restartIce = function () { let c = this; - /** @ts-ignore */ - if(typeof c.pc.restartIce === 'function') { + if('restartIce' in c.pc) { try { /** @ts-ignore */ c.pc.restartIce(); @@ -988,7 +1009,7 @@ Stream.prototype.updateStats = async function() { * setStatsInterval sets the interval in milliseconds at which the onstats * handler will be called. This is only useful for up streams. * - * @param {number} ms + * @param {number} ms - The interval in milliseconds. */ Stream.prototype.setStatsInterval = function(ms) { let c = this; diff --git a/static/sfu.js b/static/sfu.js index 13e7cba..c3dc35e 100644 --- a/static/sfu.js +++ b/static/sfu.js @@ -103,9 +103,8 @@ function storeSettings(settings) { * * @returns {settings} */ - function getSettings() { - /** @type{settings} */ + /** @type {settings} */ let settings; try { let json = window.sessionStorage.getItem('settings'); @@ -127,22 +126,50 @@ function updateSettings(settings) { storeSettings(s); } +/** + * @param {string} id + */ +function getSelectElement(id) { + let elt = document.getElementById(id); + if(!elt || !(elt instanceof HTMLSelectElement)) + throw new Error(`Couldn't find ${id}`); + return elt; +} + +/** + * @param {string} id + */ +function getInputElement(id) { + let elt = document.getElementById(id); + if(!elt || !(elt instanceof HTMLInputElement)) + throw new Error(`Couldn't find ${id}`); + return elt; +} + +/** + * @param {string} id + */ +function getButtonElement(id) { + let elt = document.getElementById(id); + if(!elt || !(elt instanceof HTMLButtonElement)) + throw new Error(`Couldn't find ${id}`); + return elt; +} + function reflectSettings() { let settings = getSettings(); let store = false; setLocalMute(settings.localMute); - let videoselect = - /** @type {HTMLSelectElement} */(document.getElementById('videoselect')); + let videoselect = getSelectElement('videoselect'); if(!settings.video || !selectOptionAvailable(videoselect, settings.video)) { settings.video = selectOptionDefault(videoselect); store = true; } videoselect.value = settings.video; - let audioselect = - /** @type {HTMLSelectElement} */(document.getElementById('audioselect')); + let audioselect = getSelectElement('audioselect'); if(!settings.audio || !selectOptionAvailable(audioselect, settings.audio)) { settings.audio = selectOptionDefault(audioselect); store = true; @@ -150,24 +177,24 @@ function reflectSettings() { audioselect.value = settings.audio; if(settings.request) - document.getElementById('requestselect').value = settings.request; + getSelectElement('requestselect').value = settings.request; else { - settings.request = document.getElementById('requestselect').value; + settings.request = getSelectElement('requestselect').value; store = true; } if(settings.send) - document.getElementById('sendselect').value = settings.send; + getSelectElement('sendselect').value = settings.send; else { - settings.send = document.getElementById('sendselect').value; + settings.send = getSelectElement('sendselect').value; store = true; } - document.getElementById('activitybox').checked = settings.activityDetection; + getInputElement('activitybox').checked = settings.activityDetection; - document.getElementById('blackboardbox').checked = settings.blackboardMode; + getInputElement('blackboardbox').checked = settings.blackboardMode; - document.getElementById('studiobox').checked = settings.studioMode; + getInputElement('studiobox').checked = settings.studioMode; if(store) storeSettings(settings); @@ -218,9 +245,9 @@ function setConnected(connected) { } else { resetUsers(); let userpass = getUserPass(); - document.getElementById('username').value = + getInputElement('username').value = userpass ? userpass.username : ''; - document.getElementById('password').value = + getInputElement('password').value = userpass ? userpass.password : ''; statspan.textContent = 'Disconnected'; statspan.classList.remove('connected'); @@ -234,6 +261,7 @@ function setConnected(connected) { } } +/** @this {ServerConnection} */ function gotConnected() { setConnected(true); let up = getUserPass(); @@ -243,6 +271,7 @@ function gotConnected() { } /** + * @this {ServerConnection} * @param {number} code * @param {string} reason */ @@ -255,6 +284,7 @@ function gotClose(code, reason) { } /** + * @this {ServerConnection} * @param {Stream} c */ function gotDownStream(c) { @@ -291,12 +321,12 @@ setViewportHeight(); addEventListener('resize', setViewportHeight); addEventListener('orientationchange', setViewportHeight); -document.getElementById('presentbutton').onclick = function(e) { +getButtonElement('presentbutton').onclick = function(e) { e.preventDefault(); addLocalMedia(); }; -document.getElementById('unpresentbutton').onclick = function(e) { +getButtonElement('unpresentbutton').onclick = function(e) { e.preventDefault(); delUpMediaKind('local'); resizePeers(); @@ -353,26 +383,34 @@ function setLocalMute(mute) { } } -document.getElementById('videoselect').onchange = function(e) { +getSelectElement('videoselect').onchange = function(e) { e.preventDefault(); + if(!(this instanceof HTMLSelectElement)) + throw new Error('Unexpected type for this'); updateSettings({video: this.value}); changePresentation(); }; -document.getElementById('audioselect').onchange = function(e) { +getSelectElement('audioselect').onchange = function(e) { e.preventDefault(); + if(!(this instanceof HTMLSelectElement)) + throw new Error('Unexpected type for this'); updateSettings({audio: this.value}); changePresentation(); }; -document.getElementById('blackboardbox').onchange = function(e) { +getInputElement('blackboardbox').onchange = function(e) { e.preventDefault(); + if(!(this instanceof HTMLInputElement)) + throw new Error('Unexpected type for this'); updateSettings({blackboardMode: this.checked}); changePresentation(); } -document.getElementById('studiobox').onchange = function(e) { +getInputElement('studiobox').onchange = function(e) { e.preventDefault(); + if(!(this instanceof HTMLInputElement)) + throw new Error('Unexpected type for this'); updateSettings({studioMode: this.checked}); changePresentation(); } @@ -413,7 +451,9 @@ function getMaxVideoThroughput() { } } -document.getElementById('sendselect').onchange = async function(e) { +getSelectElement('sendselect').onchange = async function(e) { + if(!(this instanceof HTMLSelectElement)) + throw new Error('Unexpected type for this'); updateSettings({send: this.value}); let t = getMaxVideoThroughput(); for(let id in serverConnection.up) { @@ -423,8 +463,10 @@ document.getElementById('sendselect').onchange = async function(e) { } } -document.getElementById('requestselect').onchange = function(e) { +getSelectElement('requestselect').onchange = function(e) { e.preventDefault(); + if(!(this instanceof HTMLSelectElement)) + throw new Error('Unexpected type for this'); updateSettings({request: this.value}); serverConnection.request(this.value); }; @@ -433,7 +475,9 @@ const activityDetectionInterval = 200; const activityDetectionPeriod = 700; const activityDetectionThreshold = 0.2; -document.getElementById('activitybox').onchange = function(e) { +getInputElement('activitybox').onchange = function(e) { + if(!(this instanceof HTMLInputElement)) + throw new Error('Unexpected type for this'); updateSettings({activityDetection: this.checked}); for(let id in serverConnection.down) { let c = serverConnection.down[id]; @@ -446,6 +490,10 @@ document.getElementById('activitybox').onchange = function(e) { } } +/** + * @this {Stream} + * @param {Object} stats + */ function gotUpStats(stats) { let c = this; @@ -477,8 +525,12 @@ function setActive(c, value) { peer.classList.remove('peer-active'); } +/** + * @this {Stream} + * @param {Object} stats + */ function gotDownStats(stats) { - if(!document.getElementById('activitybox').checked) + if(!getInputElement('activitybox').checked) return; let c = this; @@ -514,7 +566,11 @@ function addSelectOption(select, label, value) { if(!value) value = label; for(let i = 0; i < select.children.length; i++) { - let child = /** @type {HTMLOptionElement} */ (select.children[i]); + let child = select.children[i]; + if(!(child instanceof HTMLOptionElement)) { + console.warn('Unexpected select child'); + continue; + } if(child.value === value) { if(child.label !== label) { child.label = label; @@ -536,10 +592,13 @@ function addSelectOption(select, label, value) { function selectOptionAvailable(select, value) { let children = select.children; for(let i = 0; i < children.length; i++) { - let child = /** @type {HTMLOptionElement} */ (select.children[i]); - if(child.value === value) { - return true; + let child = select.children[i]; + if(!(child instanceof HTMLOptionElement)) { + console.warn('Unexpected select child'); + continue; } + if(child.value === value) + return true; } return false; } @@ -551,7 +610,11 @@ function selectOptionAvailable(select, value) { function selectOptionDefault(select) { /* First non-empty option. */ for(let i = 0; i < select.children.length; i++) { - let child = /** @type {HTMLOptionElement} */ (select.children[i]); + let child = select.children[i]; + if(!(child instanceof HTMLOptionElement)) { + console.warn('Unexpected select child'); + continue; + } if(child.value) return child.value; } @@ -587,13 +650,13 @@ async function setMediaChoices(done) { if(d.kind === 'videoinput') { if(!label) label = `Camera ${cn}`; - addSelectOption(document.getElementById('videoselect'), + addSelectOption(getSelectElement('videoselect'), label, d.deviceId); cn++; } else if(d.kind === 'audioinput') { if(!label) label = `Microphone ${mn}`; - addSelectOption(document.getElementById('audioselect'), + addSelectOption(getSelectElement('audioselect'), label, d.deviceId); mn++; } @@ -693,6 +756,7 @@ async function addLocalMedia(id) { stopUpMedia(old); let constraints = {audio: audio, video: video}; + /** @type {MediaStream} */ let stream = null; try { stream = await navigator.mediaDevices.getUserMedia(constraints); @@ -717,11 +781,11 @@ async function addLocalMedia(id) { t.enabled = false; } else if(t.kind == 'video') { if(settings.blackboardMode) { - if('contentHint' in t) - t.contentHint = 'detail'; + /** @ts-ignore */ + t.contentHint = 'detail'; } } - let sender = c.pc.addTrack(t, stream); + c.pc.addTrack(t, stream); }); c.onstats = gotUpStats; @@ -730,12 +794,16 @@ async function addLocalMedia(id) { setButtonsVisibility(); } -async function addShareMedia(setup) { +async function addShareMedia() { if(!getUserPass()) return; + /** @type {MediaStream} */ let stream = null; try { + if(!('getDisplayMedia' in navigator.mediaDevices)) + throw new Error('Your browser does not support screen sharing'); + /** @ts-ignore */ stream = await navigator.mediaDevices.getDisplayMedia({video: true}); } catch(e) { console.error(e); @@ -747,7 +815,7 @@ async function addShareMedia(setup) { c.kind = 'screenshare'; c.stream = stream; stream.getTracks().forEach(t => { - let sender = c.pc.addTrack(t, stream); + c.pc.addTrack(t, stream); t.onended = e => { delUpMedia(c); }; @@ -807,6 +875,9 @@ function delUpMediaKind(kind) { hideVideo(); } +/** + * @param {string} kind + */ function findUpMedia(kind) { for(let id in serverConnection.up) { if(serverConnection.up[id].kind === kind) @@ -815,6 +886,9 @@ function findUpMedia(kind) { return null; } +/** + * @param {boolean} mute + */ function muteLocalTracks(mute) { if(!serverConnection) return; @@ -846,12 +920,14 @@ function setMedia(c, isUp) { peersdiv.appendChild(div); } - let media = document.getElementById('media-' + c.id); + let media = /** @type {HTMLVideoElement} */ + (document.getElementById('media-' + c.id)); if(!media) { media = document.createElement('video'); media.id = 'media-' + c.id; media.classList.add('media'); media.autoplay = true; + /** @ts-ignore */ media.playsinline = true; media.controls = true; if(isUp) @@ -883,7 +959,9 @@ function delMedia(id) { let peer = document.getElementById('peer-' + id); if(!peer) throw new Error('Removing unknown media'); - let media = document.getElementById('media-' + id); + + let media = /** @type{HTMLVideoElement} */ + (document.getElementById('media-' + id)); media.srcObject = null; mediadiv.removeChild(peer); @@ -941,14 +1019,19 @@ function resizePeers() { if (!count) // No video, nothing to resize. return; - let size = 100 / columns; let container = document.getElementById("video-container") // Peers div has total padding of 30px, we remove 30 on offsetHeight let max_video_height = Math.trunc((peers.offsetHeight - 30) / columns); let media_list = document.getElementsByClassName("media"); - for(let i = 0; i < media_list.length; i++) - media_list[i].style['max_height'] = max_video_height + "px"; + for(let i = 0; i < media_list.length; i++) { + let media = media_list[i]; + if(!(media instanceof HTMLMediaElement)) { + console.warn('Unexpected media'); + continue; + } + media.style['max_height'] = max_video_height + "px"; + } if (count <= 2 && container.offsetHeight > container.offsetWidth) { peers.style['grid-template-columns'] = "repeat(1, 1fr)"; @@ -957,7 +1040,7 @@ function resizePeers() { } } -/** @type{Object.} */ +/** @type{Object} */ let users = {}; /** @@ -1051,7 +1134,7 @@ function clearUsername() { } /** - * @param {Object.} perms + * @param {Object} perms */ function gotPermissions(perms) { displayUsername(); @@ -1064,7 +1147,7 @@ const urlRegexp = /https?:\/\/[-a-zA-Z0-9@:%/._\\+~#=?]+[-a-zA-Z0-9@:%/_\\+~#=]/ /** * @param {string} line - * @returns {Array.} + * @returns {(Text|HTMLElement)[]} */ function formatLine(line) { let r = new RegExp(urlRegexp); @@ -1088,7 +1171,7 @@ function formatLine(line) { } /** - * @param {Array.} lines + * @param {string[]} lines * @returns {HTMLElement} */ function formatLines(lines) { @@ -1113,6 +1196,12 @@ function formatLines(lines) { /** @type {lastMessage} */ let lastMessage = {}; +/** + * @param {string} peerId + * @param {string} nick + * @param {string} kind + * @param {string} message + */ function addToChatbox(peerId, nick, kind, message){ let userpass = getUserPass(); let row = document.createElement('div'); @@ -1174,7 +1263,7 @@ function clearChat() { * part may be quoted and may include backslash escapes. * * @param {string} line - * @returns {Array.} + * @returns {string[]} */ function parseCommand(line) { let i = 0; @@ -1204,7 +1293,8 @@ function parseCommand(line) { } function handleInput() { - let input = document.getElementById('input'); + let input = /** @type {HTMLTextAreaElement} */ + (document.getElementById('input')); let data = input.value; input.value = ''; @@ -1356,39 +1446,66 @@ function chatResizer(e) { document.getElementById('resizer').addEventListener('mousedown', chatResizer, false); +/** @enum {string} */ +const MessageLevel = { + info: 'info', + warning: 'warning', + error: 'error', +} -function displayError(message, level, position, gravity) { - var background = "linear-gradient(to right, #e20a0a, #df2d2d)"; - if (level === "info") { - background = "linear-gradient(to right, #529518, #96c93d)"; - } - if (level === "warning") { - background = "linear-gradient(to right, #edd800, #c9c200)"; +/** + * @param {string} message + * @param {MessageLevel} [level] + */ +function displayError(message, level) { + if(!level) + level = MessageLevel.error; + + var background = 'linear-gradient(to right, #e20a0a, #df2d2d)'; + var position = 'center'; + var gravity = 'top'; + + switch(level) { + case MessageLevel.info: + background = 'linear-gradient(to right, #529518, #96c93d)'; + position = 'right'; + gravity = 'bottom'; + break; + case MessageLevel.warning: + background = "linear-gradient(to right, #edd800, #c9c200)"; + break; } + + /** @ts-ignore */ Toastify({ - text: message, - duration: 4000, - close: true, - position: position ? position: 'center', - gravity: gravity ? gravity : 'top', - backgroundColor: background, - className: level, + text: message, + duration: 4000, + close: true, + position: position, + gravity: gravity, + backgroundColor: background, + className: level, }).showToast(); } +/** + * @param {string} message + */ function displayWarning(message) { - let level = "warning"; - return displayError(message, level); + return displayError(message, MessageLevel.warning); } +/** + * @param {string} message + */ function displayMessage(message) { - return displayError(message, "info", "right", "bottom"); + return displayError(message, MessageLevel.info); } document.getElementById('userform').onsubmit = function(e) { e.preventDefault(); - let username = document.getElementById('username').value.trim(); - let password = document.getElementById('password').value; + let username = getInputElement('username').value.trim(); + let password = getInputElement('password').value; storeUserPass(username, password); serverConnect(); }; @@ -1438,6 +1555,8 @@ document.getElementById('clodeside').onclick = function(e) { document.getElementById('collapse-video').onclick = function(e) { e.preventDefault(); + if(!(this instanceof HTMLElement)) + throw new Error('Unexpected type for this'); let width = window.innerWidth; if (width <= 768) { let user_box = document.getElementById('userDropdown'); @@ -1453,6 +1572,8 @@ document.getElementById('collapse-video').onclick = function(e) { document.getElementById('switch-video').onclick = function(e) { e.preventDefault(); + if(!(this instanceof HTMLElement)) + throw new Error('Unexpected type for this'); showVideo(); this.style.display = ""; document.getElementById('collapse-video').style.display = "block"; diff --git a/static/tsconfig.json b/static/tsconfig.json index ca34d5d..a47eef7 100644 --- a/static/tsconfig.json +++ b/static/tsconfig.json @@ -4,10 +4,16 @@ "allowJs": true, "checkJs": true, "declaration": true, + "noImplicitThis": true, "emitDeclarationOnly": true, - "strictBindCallApply": true + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true }, "files": [ - "protocol.js" + "protocol.js", + "sfu.js" ] }