Commit 0aa77441 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Improve typing of javascript code.

We now enable typing of sfu.js.
parent 4e14c29f
......@@ -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.<string,Stream>}
*
* @type {Object<string,Stream>}
*/
this.up = {};
/**
* The set of all down streams, indexed by their id.
* @type {Object.<string,Stream>}
*
* @type {Object<string,Stream>}
*/
this.down = {};
/**
* The ICE configuration used by all associated streams.
* @type {Array.<RTCIceServer>}
*
* @type {RTCIceServer[]}
*/
this.iceServers = null;
/**
* The permissions granted to this connection.
* @type {Object.<string,boolean>}
*
* @type {Object<string,boolean>}
*/
this.permissions = {};
/**
* clientdata is a convenient place to attach data to a ServerConnection.
* It is not used by the library.
* @type{Object.<string,any>}
*
* @type{Object<any,any>}
*/
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.<string,boolean>): any}
*
* @type{(this: ServerConnection, permissions: Object<string,boolean>) => 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.<string,boolean>} [permissions]
* @property {Object<string,boolean>} [permissions]
* @property {string} [group]
* @property {string} [value]
* @property {RTCSessionDescriptionInit} [offer]
* @property {RTCSessionDescriptionInit} [answer]
* @property {RTCIceCandidate} [candidate]
* @property {Object.<string,string>} [labels]
* @property {Object.<string,(boolean|number)>} [request]
* @property {Object<string,string>} [labels]
* @property {Object<string,(boolean|number)>} [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<Array.<Object>>}
* @returns {Promise<RTCIceServer[]>}
* @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.<string,boolean>} */
/** @type {Object<string,boolean>} */
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.<string, string>} labels
* @param {Object<string, string>} 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.<string,string>}
* @type {Object<string,string>}
*/
this.labels = {};
/**
* Track labels, indexed by mid.
*
* @type {Object.<string,string>}
* @type {Object<string,string>}
*/
this.labelsByMid = {};
/**
* Buffered ICE candidates. This will be flushed by flushIceCandidates
* when the PC becomes stable.
*
* @type {Array.<RTCIceCandidate>}
* @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.<string,any>}
* @type {Object<string,any>}
*/
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.<string,any>}
* @type{Object<any,any>}
*/
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.<string,any>): any}
* @type{(this: Stream, stats: Object<any,any>) => any}
*/
this.onstats = null;
}
......@@ -834,6 +855,7 @@ Stream.prototype.close = function(sendclose) {
* @function
*/
Stream.prototype.flushIceCandidates = async function () {
/** @type {Promise<any>[]} */
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;
......
......@@ -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<string,any>} stats
*/
function gotUpStats(stats) {
let c = this;
......@@ -477,8 +525,12 @@ function setActive(c, value) {
peer.classList.remove('peer-active');
}
/**
* @this {Stream}
* @param {Object<string,any>} 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.<string,string>} */
/** @type{Object<string,string>} */
let users = {};
/**
......@@ -1051,7 +1134,7 @@ function clearUsername() {
}
/**
* @param {Object.<string,boolean>} perms
* @param {Object<string,boolean>} 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.<Text|HTMLElement>}
* @returns {(Text|HTMLElement)[]}
*/
function formatLine(line) {
let r = new RegExp(urlRegexp);
......@@ -1088,7 +1171,7 @@ function formatLine(line) {
}
/**
* @param {Array.<string>} 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.<string>}
* @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";
......
......@@ -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"
]
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment