AIRA/src/frontend/index.js

1269 lines
48 KiB
JavaScript

"use strict";
let identityName = undefined;
let socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");;
let notificationAllowed = false;
let localIps = [];
let currentSessionId = -1;
let sessionsData = new Map();
let msgHistory = new Map();
let pendingMsgs = new Map();
let pendingFilesTransfers = new Map();
let avatarTimestamps = new Map([
["self", Date.now()]
]);
function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId");
if (sessionId != null && socket.readyState === WebSocket.OPEN) {
currentSessionId = sessionId;
let session = sessionsData.get(sessionId);
if (!session.seen) {
session.seen = true;
socket.send("set_seen "+sessionId);
}
displaySessions();
displayHeader();
displayChatBottom();
displayHistory();
}
}
let ip_input = document.getElementById("ip_input");
ip_input.addEventListener("keyup", function(event) {
if (event.key === "Enter") {
socket.send("connect "+ip_input.value);
ip_input.value = "";
}
});
document.getElementById("show_local_ips").onclick = function() {
let mainDiv = document.createElement("div");
let h2Title = document.createElement("h2");
h2Title.textContent = "Your IP addresses:";
mainDiv.appendChild(h2Title);
let ul = document.createElement("ul");
ul.classList.add("ips");
for (let i=0; i<localIps.length; ++i) {
let li = document.createElement("li");
li.textContent = localIps[i];
ul.appendChild(li);
}
mainDiv.appendChild(ul);
showPopup(mainDiv);
};
let message_input = document.getElementById("message_input");
message_input.addEventListener("keyup", function(event) {
if (event.key === "Enter") {
socket.send("send "+currentSessionId+" "+message_input.value);
message_input.value = "";
}
});
document.getElementById("delete_conversation").onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p1 = document.createElement("p");
p1.textContent = "Deleting a conversation only affects you. Your contact will still have a copy of this conversation if he/she doesn't delete it too.";
let p2 = document.createElement("p");
p2.textContent = "Do you really want to delete all this conversation (messages and files) ?";
mainDiv.appendChild(p1);
mainDiv.appendChild(p2);
let button = document.createElement("button");
button.classList.add("classic_button");
button.textContent = "Delete";
button.onclick = function() {
socket.send("delete_conversation "+currentSessionId);
msgHistory.get(currentSessionId).length = 0;
removePopup();
displayHistory();
};
mainDiv.appendChild(button);
showPopup(mainDiv);
};
document.getElementById("add_contact").onclick = function() {
socket.send("contact "+currentSessionId);
sessionsData.get(currentSessionId).isContact = true;
pendingMsgs.set(currentSessionId, []);
displayHeader();
displaySessions();
};
document.getElementById("remove_contact").onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p1 = document.createElement("p");
p1.textContent = "Deleting contact will remove her/his identity key and your conversation (messages and files). You won\'t be able to recognize her/him anymore. This action only affects you.";
mainDiv.appendChild(p1);
let p2 = document.createElement("p");
p2.textContent = "Do you really want to remove this contact ?";
mainDiv.appendChild(p2);
let button = document.createElement("button");
button.classList.add("classic_button");
button.textContent = "Delete";
button.onclick = function() {
socket.send("uncontact "+currentSessionId);
let session = sessionsData.get(currentSessionId);
session.isContact = false;
session.isVerified = false;
if (!session.isOnline) {
sessionsData.delete(currentSessionId);
msgHistory.get(currentSessionId).length = 0;
displayChatBottom();
}
pendingMsgs.delete(currentSessionId);
displayHeader();
displaySessions();
displayHistory();
removePopup();
};
mainDiv.appendChild(button);
showPopup(mainDiv);
};
document.getElementById("verify").onclick = function() {
let session = sessionsData.get(currentSessionId);
if (typeof session !== "undefined") {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let instructions = document.createElement("p");
instructions.textContent = "Compare the following fingerprints by a trusted way of communication (such as real life) and be sure they match.";
mainDiv.appendChild(instructions);
let p_local = document.createElement("p");
p_local.textContent = "Local fingerprint:";
mainDiv.appendChild(p_local);
let pre_local = document.createElement("pre");
pre_local.textContent = beautifyFingerprint(identityFingerprint);
mainDiv.appendChild(pre_local);
let p_peer = document.createElement("p");
p_peer.textContent = "Peer fingerprint:";
mainDiv.appendChild(p_peer);
let pre_peer = document.createElement("pre");
pre_peer.textContent = beautifyFingerprint(session.fingerprint);
mainDiv.appendChild(pre_peer);
let buttonRow = document.createElement("div");
buttonRow.classList.add("button_row");
let verifyButton = document.createElement("button");
verifyButton.classList.add("classic_button");
verifyButton.textContent = "They match";
verifyButton.onclick = function() {
socket.send("verify "+currentSessionId);
sessionsData.get(currentSessionId).isVerified = true;
removePopup();
displayHeader();
displaySessions();
};
buttonRow.appendChild(verifyButton);
let cancelButton = document.createElement("button");
cancelButton.classList.add("classic_button");
cancelButton.textContent = "They don't match";
cancelButton.onclick = removePopup;
buttonRow.appendChild(cancelButton);
mainDiv.appendChild(buttonRow);
showPopup(mainDiv);
}
};
document.getElementById("logout").onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p_warning = document.createElement("p");
p_warning.textContent = "If you log out, you will no longer receive messages and pending messages will not be sent until you log in back.";
mainDiv.appendChild(p_warning);
let p_ask = document.createElement("p");
p_ask.textContent = "Do you really want to log out ?";
mainDiv.appendChild(p_ask);
let button = document.createElement("button");
button.classList.add("classic_button");
button.textContent = "Log out";
button.onclick = logout;
mainDiv.appendChild(button);
showPopup(mainDiv);
};
document.getElementById("attach_file").onchange = function(event) {
let files = event.target.files;
let useLargeFileTransfer = false;
for (let i=0; i<files.length; ++i) {
if (files[i].size > 16380000) {
useLargeFileTransfer = true;
break;
}
}
if (useLargeFileTransfer) {
if (pendingFilesTransfers.has(currentSessionId)) {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p = document.createElement("p");
p.textContent = "Another file transfer is already in progress.";
mainDiv.appendChild(p);
showPopup(mainDiv);
} else {
let filesTransfer = [];
let fileInfo = "";
for (let i=0; i<files.length; ++i) {
filesTransfer.push({
"file": files[i],
"name": files[i].name,
"size": files[i].size,
"transferred": 0,
"lastChunk": Date.now()
});
fileInfo += ' '+files[i].size+' '+b64EncodeUnicode(files[i].name);
};
pendingFilesTransfers.set(currentSessionId, {
"files": filesTransfer,
"index": 0,
"state": "waiting",
});
socket.send("large_files "+currentSessionId+fileInfo);
displayChatBottom();
}
} else {
for (let i=0; i<files.length; ++i) {
let formData = new FormData();
formData.append("session_id", currentSessionId);
formData.append("", files[i]);
fetch("/send_file", {method: "POST", body: formData}).then(response => {
if (response.ok) {
response.text().then(text => {
if (text === "pending") {
newPendingMsg(currentSessionId, true, files[i].name);
}
});
} else {
console.log(response);
}
});
};
}
};
document.getElementById("file_cancel").onclick = function() {
socket.send("abort "+currentSessionId);
};
let msgLog = document.getElementById("msg_log");
msgLog.onscroll = function() {
let session = sessionsData.get(currentSessionId);
if (typeof session !== "undefined") {
if (session.isContact) {
if (msgLog.scrollTop < 30) {
socket.send("load_msgs "+currentSessionId);
}
}
}
};
let profileDiv = document.querySelector("#me>div");
profileDiv.onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.id = "profile_info";
let avatarContainer = document.createElement("div");
avatarContainer.id = "avatarContainer";
let labelAvatar = document.createElement("label");
labelAvatar.setAttribute("for", "avatar_input");
let inputAvatar = document.createElement("input");
inputAvatar.type = "file";
inputAvatar.accept = "image/*";
inputAvatar.id = "avatar_input";
inputAvatar.onchange = function(event) {
uploadAvatar(event, function() {
avatarTimestamps.set("self", Date.now());
refreshSelfAvatar();
});
};
labelAvatar.appendChild(inputAvatar);
labelAvatar.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let uploadP = document.createElement("p");
uploadP.textContent = "Upload";
labelAvatar.appendChild(uploadP);
avatarContainer.appendChild(labelAvatar);
let removeAvatar = document.createElement("span");
removeAvatar.id = "removeAvatar";
removeAvatar.textContent = "Remove";
removeAvatar.onclick = function() {
socket.send("remove_avatar");
};
avatarContainer.appendChild(removeAvatar);
mainDiv.appendChild(avatarContainer);
let sectionName = document.createElement("section");
let titleName = document.createElement("h3");
titleName.textContent = "Name:";
sectionName.appendChild(titleName);
let inputName = document.createElement("input");
inputName.id = "new_name";
inputName.type = "text";
inputName.value = identityName;
sectionName.appendChild(inputName);
let saveNameButton = document.createElement("button");
saveNameButton.classList.add("classic_button");;
saveNameButton.textContent = "Save";
saveNameButton.onclick = function() {
socket.send("change_name "+document.getElementById("new_name").value);
};
sectionName.appendChild(saveNameButton);
mainDiv.appendChild(sectionName);
let sectionFingerprint = document.createElement("section");
let titleFingerprint = document.createElement("h3");
titleFingerprint.textContent = "Your fingerprint:";
sectionFingerprint.appendChild(titleFingerprint);
let fingerprint = document.createElement("pre");
fingerprint.textContent = beautifyFingerprint(identityFingerprint);
sectionFingerprint.appendChild(fingerprint);
mainDiv.appendChild(sectionFingerprint);
let sectionPadding = document.createElement("section");
sectionPadding.appendChild(generateSwitchPreference("Use PSEC padding", "PSEC padding obfuscates the length of your messages but uses more network bandwidth.", usePadding, function(checked) {
socket.send("set_use_padding "+checked);
usePadding = checked;
}));
mainDiv.appendChild(sectionPadding);
let sectionPassword = document.createElement("section");
let titlePassword = document.createElement("h3");
titlePassword.textContent = "Change your password:";
sectionPassword.appendChild(titlePassword);
if (isIdentityProtected) {
let input_old_password = document.createElement("input");
input_old_password.type = "password";
input_old_password.placeholder = "Current password";
sectionPassword.appendChild(input_old_password);
}
let inputPassword1 = document.createElement("input");
let inputPassword2 = document.createElement("input");
inputPassword1.type = "password";
inputPassword1.placeholder = "New password (empty for no password)";
inputPassword2.type = "password";
inputPassword2.placeholder = "New password (confirmation)";
sectionPassword.appendChild(inputPassword1);
sectionPassword.appendChild(inputPassword2);
let errorMsg = document.createElement("p");
errorMsg.id = "password_errorMsg";
errorMsg.style.color = "red";
sectionPassword.appendChild(errorMsg);
let changePasswordButton = document.createElement("button");
changePasswordButton.classList.add("classic_button");
changePasswordButton.textContent = "Change password";
changePasswordButton.onclick = function() {
let inputs = document.querySelectorAll("input[type=\"password\"]");
let newPassword, newPasswordConfirm, oldPassword;
if (isIdentityProtected) {
oldPassword = inputs[0].value;
newPassword = inputs[1].value;
newPasswordConfirm = inputs[2].value;
} else {
newPassword = inputs[0].value;
newPasswordConfirm = inputs[1].value;
}
if (newPassword == newPasswordConfirm) {
let newPasswordSet = newPassword.length > 0;
if (isIdentityProtected && oldPassword.length == 0) {
errorMsg.textContent = "Current password cannot be empty.";
} else if (isIdentityProtected || newPasswordSet) { //don't change password if identity is not protected and new password is blank
let msg = "change_password";
if (isIdentityProtected) {
msg += " "+b64EncodeUnicode(inputs[0].value);
}
if (newPasswordSet) {
msg += " "+b64EncodeUnicode(newPassword);
}
socket.send(msg);
} else {
removePopup();
}
} else {
newPassword = "";
newPasswordConfirm = "";
errorMsg.textContent = "Passwords don't match.";
}
};
sectionPassword.appendChild(changePasswordButton);
mainDiv.appendChild(sectionPassword);
let sectionDelete = document.createElement("section");
let deleteTitle = document.createElement("h3");
deleteTitle.textContent = "Delete identity:";
sectionDelete.appendChild(deleteTitle);
sectionDelete.style.borderTop = "1px solid red";
let p = document.createElement("p");
p.textContent = "Deleting your identity will delete all your conversations (messages and files), all your contacts, and your private key. You won't be able to be recognized by your contacts anymore.";
p.style.color = "red";
sectionDelete.appendChild(p);
let deleteButton = document.createElement("button");
deleteButton.classList.add("classic_button");
deleteButton.textContent = "Delete";
deleteButton.style.backgroundColor = "red";
deleteButton.onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p = document.createElement("p");
p.textContent = "This action is irreversible. Are you sure you want to delete all your data ?";
mainDiv.appendChild(p);
let deleteButton = document.createElement("button");
deleteButton.classList.add("classic_button");
deleteButton.style.backgroundColor = "red";
deleteButton.textContent = "Delete";
deleteButton.onclick = function() {
socket.send("disappear");
};
mainDiv.appendChild(deleteButton);
showPopup(mainDiv);
};
sectionDelete.appendChild(deleteButton);
mainDiv.appendChild(sectionDelete);
showPopup(mainDiv);
};
let chatHeader = document.getElementById("chat_header");
chatHeader.children[0].onclick = showSessionInfoPopup;
document.querySelector("#refresher button").onclick = function() {
socket.send("refresh");
};
//source: https://stackoverflow.com/a/14919494
function humanFileSize(bytes, dp=1) {
const thresh = 1000;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
const r = 10**dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
//source: https://stackoverflow.com/a/30106551
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function b64DecodeUnicode(str) {
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
//source: https://www.w3schools.com/js/js_cookies.asp
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function parseTimestamp(timestamp) {
return new Date(Number(timestamp) * 1000);
}
function toTwoNumbers(n) {
if (n < 10) {
return '0'+n;
} else {
return n;
}
}
socket.onopen = function() {
console.log("Connected");
socket.send(getCookie("aira_auth")); //authenticating websocket connection
window.onfocus = function() {
if (currentSessionId != -1) {
socket.send("set_seen "+currentSessionId);
}
};
if (Notification.permission === "granted") {
notificationAllowed = true;
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
notificationAllowed = true;
}
});
}
};
socket.onmessage = function(msg) {
if (typeof msg.data == "string") {
console.log("Message: "+msg.data);
let args = msg.data.split(" ");
switch (args[0]) {
case "inc_file_transfer":
onIncFilesTransfer(args[1], parseInt(args[2]));
break;
case "new_session":
onNewSession(args[1], args[2] === "true", args[3], args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5));
break;
case "new_message":
onNewMessage(args[1], args[2] === "true", parseTimestamp(args[3]), msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
break;
case "file":
onNewFile(args[1], args[2] === "true", parseTimestamp(args[3]), args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5));
break;
case "files_transfer":
onNewFilesTransfer(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "ask_large_files":
onAskLargeFiles(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "files_accepted":
onFilesAccepted(args[1]);
break;
case "aborted":
onFilesTransferAborted(args[1]);
break;
case "load_msgs":
onMsgsLoad(args[1], msg.data.slice(args[0].length+args[1].length+2));
break;
case "name_told":
onNameTold(args[1], msg.data.slice(args[0].length+args[1].length+2));
break;
case "avatar_changed":
onAvatarChanged(args[1]);
break;
case "is_contact":
onIsContact(args[1], args[2] === "true", args[3], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
break;
case "not_seen":
setNotSeen(msg.data.slice(args[0].length+1));
break;
case "pending":
newPendingMsg(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "sending_pending_msgs":
onSendingPendingMsgs(args[1]);
break;
case "pending_msgs_sent":
onPendingMsgsSent(args[1]);
break;
case "local_ips":
setLocalIps(msg.data.slice(args[0].length+1));
break;
case "set_name":
onNameSet(msg.data.slice(args[0].length+1));
break;
case "password_changed":
onPasswordChanged(args[1] === "true", args[2] === "true");
break;
case "disconnected":
onDisconnected(args[1]);
break;
case "logout":
logout();
}
}
};
socket.onclose = function() {
console.log("Disconnected");
currentSessionId = -1;
displayHistory();
displayHeader();
displayChatBottom();
displaySessions();
document.getElementById("disconnected").classList.add("disconnected");
};
function onNewSession(sessionId, outgoing, fingerprint, ip, name) {
if (sessionsData.has(sessionId)) {
let session = sessionsData.get(sessionId);
session.isOnline = true;
session.outgoing = outgoing;
session.ip = ip;
displaySessions();
if (currentSessionId == sessionId) {
displayChatBottom();
}
} else {
addSession(sessionId, name, outgoing, fingerprint, ip, false, false, true);
}
}
function onNameTold(sessionId, name) {
sessionsData.get(sessionId).name = name;
if (sessionId == currentSessionId) {
displayHeader();
if (document.getElementById("session_info") !== null) {
removePopup();
showSessionInfoPopup();
}
}
displaySessions();
}
function onAvatarChanged(sessionIdOrSelf) {
avatarTimestamps.set(sessionIdOrSelf, Date.now());
displaySessions();
if (sessionIdOrSelf === currentSessionId) {
displayHeader();
displayHistory(false);
refreshAvatar("#session_info .avatar", sessionIdOrSelf);
} else if (sessionIdOrSelf === "self") {
refreshSelfAvatar();
}
}
function setNotSeen(strSessionIds) {
let sessionIds = strSessionIds.split(' ');
for (let i=0; i<sessionIds.length; ++i) {
sessionsData.get(sessionIds[i]).seen = false;
}
displaySessions();
}
function newPendingMsg(sessionId, isFile, data) {
pendingMsgs.get(sessionId).push([isFile, data]);
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onSendingPendingMsgs(sessionId) {
document.getElementById("pending_msgs_indicator").classList.add("sending");
pendingMsgs.get(sessionId).length = 0;
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onPendingMsgsSent(sessionId) {
document.getElementById("pending_msgs_indicator").classList.remove("sending");
}
function setLocalIps(strIPs) {
localIps = strIPs.split(' ');
}
function onIsContact(sessionId, verified, fingerprint, name) {
if (sessionsData.has(sessionId)) {
let session = sessionsData.get(sessionId);
session.isContact = true;
session.isVerified = verified;
onNameTold(sessionId, name);
} else {
addSession(sessionId, name, undefined, fingerprint, undefined, true, verified, false);
}
pendingMsgs.set(sessionId, []);
}
function onMsgOrFileReceived(sessionId, outgoing, body) {
if (currentSessionId == sessionId) {
displayHistory();
if (!document.hidden && !outgoing) {
socket.send("set_seen "+sessionId);
}
} else {
sessionsData.get(sessionId).seen = false;
displaySessions();
}
if (document.hidden && !outgoing) {
if (notificationAllowed) {
let sessionName = sessionsData.get(sessionId).name;
new Notification(sessionName, {
"body": body,
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
}
function onNewMessage(sessionId, outgoing, timestamp, msg) {
msgHistory.get(sessionId).push([outgoing, timestamp, false, msg]);
onMsgOrFileReceived(sessionId, outgoing, msg);
}
function onNewFile(sessionId, outgoing, timestamp, uuid, filename) {
msgHistory.get(sessionId).push([outgoing, timestamp, true, [uuid, filename]]);
onMsgOrFileReceived(sessionId, outgoing, filename);
}
function onNewFilesTransfer(sessionId, index, filesInfo) {
let split = filesInfo.split(' ');
let files = [];
for (let i=0; i<split.length; i += 4) {
files.push({
"file": undefined,
"name": b64DecodeUnicode(split[i]),
"size": parseInt(split[i+1]),
"transferred": parseInt(split[i+2]),
"lastChunk": parseInt(split[i+3])
});
}
pendingFilesTransfers.set(sessionId, {
"files": files,
"index": parseInt(index),
"state": "transferring"
});
if (currentSessionId == sessionId) {
displayChatBottom();
}
}
function onAskLargeFiles(sessionId, encodedDownloadLocation, filesInfo) {
let sessionName = sessionsData.get(sessionId).name;
let mainDiv = document.createElement("div");
let h2 = document.createElement("h2");
h2.textContent = sessionName+" wants to send you some files:";
mainDiv.appendChild(h2);
let ul = document.createElement("ul");
let split = filesInfo.split(' ');
for (let i=0; i<split.length; i += 2) {
let p = document.createElement("p");
generateFileInfo(b64DecodeUnicode(split[i]), parseInt(split[i+1]), p);
let li = document.createElement("li");
li.appendChild(p);
ul.appendChild(li);
}
mainDiv.appendChild(ul);
let spanDownloadLocation = document.createElement("span");
spanDownloadLocation.textContent = b64DecodeUnicode(encodedDownloadLocation);
let pQuestion = document.createElement("p");
pQuestion.appendChild(document.createTextNode("Download them in "));
pQuestion.appendChild(spanDownloadLocation);
pQuestion.appendChild(document.createTextNode(" ?"));
mainDiv.appendChild(pQuestion);
let buttonRow = document.createElement("div");
buttonRow.classList.add("button_row");
let buttonDownload = document.createElement("button");
buttonDownload.classList.add("classic_button");
buttonDownload.textContent = "Download";
buttonDownload.onclick = function() {
removePopup();
let files = [];
for (let i=0; i<split.length; i += 2) {
files.push({
"file": undefined,
"name": b64DecodeUnicode(split[i]),
"size": parseInt(split[i+1]),
"transferred": 0,
"lastChunk": Date.now()
});
}
pendingFilesTransfers.set(sessionId, {
"files": files,
"index": 0,
"state": "transferring"
});
socket.send("download "+sessionId);
if (currentSessionId == sessionId) {
displayChatBottom();
}
};
buttonRow.appendChild(buttonDownload);
let buttonRefuse = document.createElement("button");
buttonRefuse.classList.add("classic_button");
buttonRefuse.textContent = "Refuse";
buttonRefuse.onclick = function() {
removePopup();
socket.send("abort "+sessionId);
};
buttonRow.appendChild(buttonRefuse);
mainDiv.appendChild(buttonRow);
showPopup(mainDiv, false);
if (document.hidden && notificationAllowed) {
new Notification(sessionName, {
"body": "Files download request",
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
function onFilesAccepted(sessionId) {
if (pendingFilesTransfers.has(sessionId)) {
sendNextLargeFile(sessionId);
}
}
function onFilesTransferAborted(sessionId) {
if (pendingFilesTransfers.has(sessionId)) {
pendingFilesTransfers.get(sessionId).state = "aborted";
if (sessionId == currentSessionId) {
displayChatBottom();
}
}
}
function onIncFilesTransfer(sessionId, chunkSize) {
if (pendingFilesTransfers.has(sessionId)) {
let filesTransfer = pendingFilesTransfers.get(sessionId);
let fileTransfer = filesTransfer.files[filesTransfer.index];
fileTransfer.transferred += chunkSize;
let now = Date.now();
let speed = chunkSize/(now-fileTransfer.lastChunk)*1000;
fileTransfer.lastChunk = now;
if (fileTransfer.transferred >= fileTransfer.size) {
if (filesTransfer.index == filesTransfer.files.length-1) {
filesTransfer.state = "completed";
socket.send("sending_ended "+sessionId);
} else {
filesTransfer.index += 1;
if (typeof fileTransfer.file !== "undefined") {
sendNextLargeFile(sessionId);
}
}
}
if (currentSessionId == sessionId) {
displayChatBottom(speed);
}
}
}
function onMsgsLoad(sessionId, strMsgs) {
let msgs = strMsgs.split(' ');
if (msgs.length > 3) {
let n = 0;
while (n < msgs.length) {
let outgoing = msgs[n+1] === "true";
let timestamp = parseTimestamp(msgs[n+2]);
switch (msgs[n]) {
case 'm':
let msg = b64DecodeUnicode(msgs[n+3]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, false, msg]);
n += 4;
break;
case 'f':
let uuid = msgs[n+3];
let fileName = b64DecodeUnicode(msgs[n+4]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, true, [uuid, fileName]]);
n += 5;
}
}
if (currentSessionId == sessionId) {
if (msgLog.scrollHeight - msgLog.scrollTop === msgLog.clientHeight) {
displayHistory();
} else {
let backupHeight = msgLog.scrollHeight;
displayHistory(false);
msgLog.scrollTop = msgLog.scrollHeight-backupHeight;
}
}
}
}
function onDisconnected(sessionId) {
pendingFilesTransfers.delete(sessionId);
let session = sessionsData.get(sessionId);
if (session.isContact) {
session.isOnline = false;
} else {
sessionsData.delete(sessionId);
}
if (currentSessionId == sessionId) {
displayChatBottom();
scrollHistoryToBottom();
}
if (currentSessionId == sessionId && !session.isContact) {
currentSessionId = -1;
chatHeader.classList.add("offline");
}
displaySessions();
}
function onNameSet(newName) {
removePopup();
identityName = newName;
displayProfile();
}
function onPasswordChanged(success, isProtected) {
if (success) {
removePopup();
isIdentityProtected = isProtected;
} else {
let input = document.querySelector("input[type=\"password\"]");
input.value = "";
let errorMsg = document.getElementById("password_errorMsg");
errorMsg.textContent = "Operation failed. Please check your old password.";
}
}
function sendNextLargeFile(sessionId) {
let filesTransfer = pendingFilesTransfers.get(sessionId);
filesTransfer.state = "transferring";
let fileTransfer = filesTransfer.files[filesTransfer.index];
fileTransfer.lastChunk = Date.now();
if (currentSessionId == sessionId) {
displayChatBottom();
}
let formData = new FormData();
formData.append("session_id", currentSessionId);
formData.append("", fileTransfer.file);
fetch("/send_large_file", {method: "POST", body: formData}).then(response => {
if (!response.ok) {
console.log(response);
}
});
}
function refreshAvatar(selector, sessionId) {
let avatar = document.querySelector(selector);
if (avatar !== null) {
if (typeof sessionId === "undefined") {
avatar.src = "/avatar/self?"+avatarTimestamps.get("self");
} else {
avatar.src = "/avatar/"+sessionId+"/"+sessionsData.get(sessionId).name+"?"+avatarTimestamps.get(sessionId);
}
}
}
function refreshSelfAvatar() {
refreshAvatar("#avatarContainer .avatar");
displayProfile();
if (currentSessionId != -1) {
displayHistory(false);
}
}
function beautifyFingerprint(f) {
for (let i=4; i<f.length; i+=5) {
f = f.slice(0, i)+" "+f.slice(i);
}
return f;
}
function generateSessionField(name, value) {
let div = document.createElement("div");
div.classList.add("session_field");
let pName = document.createElement("p");
pName.textContent = name+':';
div.appendChild(pName);
let pValue = document.createElement("p");
pValue.textContent = value;
div.appendChild(pValue);
return div;
}
function showSessionInfoPopup() {
let session = sessionsData.get(currentSessionId);
if (typeof session !== "undefined") {
let mainDiv = document.createElement("div");
mainDiv.id = "session_info";
mainDiv.appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
let nameDiv = document.createElement("div");
nameDiv.classList.add("name");
let h2 = document.createElement("h2");
h2.textContent = session.name;
nameDiv.appendChild(h2);
if (session.isOnline) {
let button = document.createElement("button");
button.onclick = function() {
socket.send("refresh_profile "+currentSessionId);
};
nameDiv.appendChild(button);
}
mainDiv.appendChild(nameDiv);
if (session.isOnline) {
mainDiv.appendChild(generateSessionField("Peer IP", session.ip));
let connection;
if (session.outgoing) {
connection = generateSessionField("Connection", "outgoing");
} else {
connection = generateSessionField("Connection", "incoming");
}
mainDiv.appendChild(connection);
}
if (session.isContact) {
mainDiv.appendChild(generateSessionField("Is contact", "yes"));
let isVerified;
if (session.isVerified) {
isVerified = generateSessionField("Is verified", "yes");
} else {
isVerified = generateSessionField("Is verified", "no");
}
mainDiv.appendChild(isVerified);
} else {
mainDiv.appendChild(generateSessionField("Is contact", "no"));
}
mainDiv.appendChild(generateSessionField("Fingerprint", beautifyFingerprint(session.fingerprint)));
showPopup(mainDiv);
}
}
function addSession(sessionId, name, outgoing, fingerprint, ip, isContact, isVerified, isOnline) {
sessionsData.set(sessionId, {
"name": name,
"outgoing": outgoing,
"fingerprint": fingerprint,
"ip": ip,
"isContact": isContact,
"isVerified": isVerified,
"seen": true,
"isOnline": isOnline,
});
msgHistory.set(sessionId, []);
avatarTimestamps.set(sessionId, Date.now());
displaySessions();
}
function displaySessions() {
let onlineSessions = document.getElementById("online_sessions");
onlineSessions.innerHTML = "";
let offlineSessions = document.getElementById("offline_sessions");
offlineSessions.innerHTML = "";
sessionsData.forEach(function (session, sessionId) {
let sessionElement = generateSession(sessionId, session);
if (session.isOnline) {
onlineSessions.appendChild(sessionElement);
} else {
offlineSessions.appendChild(sessionElement) ;
}
});
}
function logout() {
window.location = "/logout";
}
function displayProfile() {
profileDiv.innerHTML = "";
profileDiv.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profileDiv.appendChild(p);
}
function displayHeader() {
chatHeader.children[0].innerHTML = "";
chatHeader.className = 0;
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
chatHeader.style.display = "none";
} else {
chatHeader.children[0].appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
chatHeader.children[0].appendChild(generateName(session.name));
chatHeader.style.display = "flex";
if (session.isContact) {
chatHeader.classList.add("is_contact");
if (session.isVerified) {
chatHeader.classList.add("is_verified");
}
}
}
}
function generatePopupWarningTitle() {
let h2 = document.createElement("h2");
h2.classList.add("warning");
h2.textContent = "Warning!";
return h2;
}
function generateSwitchPreference(title, summary, checked, onSwitch) {
let label = document.createElement("label");
label.classList.add("switch_preference");
let divDesc = document.createElement("div");
divDesc.classList.add("preference_description");
let h3 = document.createElement("h3");
h3.textContent = title;
divDesc.appendChild(h3);
let pSummary = document.createElement("p");
pSummary.textContent = summary;
divDesc.appendChild(pSummary);
label.appendChild(divDesc);
let switchDiv = document.createElement("div");
switchDiv.classList.add("switch");
let input = document.createElement("input");
input.type = "checkbox";
input.checked = checked;
input.onchange = function() {
onSwitch(input.checked);
};
switchDiv.appendChild(input);
let span = document.createElement("span");
switchDiv.appendChild(span);
label.appendChild(switchDiv);
return label;
}
function generateName(name) {
let p = document.createElement("p");
if (typeof name == "undefined") {
p.appendChild(document.createTextNode("Unknown"));
} else {
p.appendChild(document.createTextNode(name));
}
return p;
}
function generateSession(sessionId, session) {
let li = document.createElement("li");
li.setAttribute("data-sessionId", sessionId);
li.appendChild(generateAvatar(sessionId, session.name, avatarTimestamps.get(sessionId)));
li.appendChild(generateName(session.name));
if (session.isContact) {
li.classList.add("is_contact");
}
if (session.isVerified) {
li.classList.add("is_verified");
}
if (!session.seen) {
li.classList.add("not_seen");
}
if (sessionId == currentSessionId) {
li.classList.add("current");
}
li.onclick = onClickSession;
return li;
}
function generateMsgHeader(name, sessionId) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(name));
let div = document.createElement("div");
div.classList.add("header");
let avatar;
if (typeof sessionId === "undefined") {
avatar = generateSelfAvatar(avatarTimestamps.get("self"));
} else {
avatar = generateAvatar(sessionId, name, avatarTimestamps.get(sessionId));
}
div.appendChild(avatar);
div.appendChild(p);
return div;
}
function generateMessageTimestamp(timestamp) {
let p = document.createElement("p");
p.classList.add("timestamp");
p.title = timestamp;
p.textContent = toTwoNumbers(timestamp.getHours())+":"+toTwoNumbers(timestamp.getMinutes());
return p;
}
function generateMessage(name, sessionId, msg) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(msg));
let div = document.createElement("div");
div.classList.add("content");
div.appendChild(linkifyElement(p));
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div);
return divContainer;
}
function generateFile(name, sessionId, outgoing, fileInfo) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
let div2 = document.createElement("div");
let h4 = document.createElement("h4");
if (outgoing) {
h4.textContent = "File sent:";
} else {
h4.textContent = "File received:";
}
div2.appendChild(h4);
div1.appendChild(div2);
let p = document.createElement("p");
if (typeof fileInfo === "string") { //pending
p.textContent = fileInfo;
} else {
p.textContent = fileInfo[1];
let a = document.createElement("a");
a.href = "/load_file?uuid="+fileInfo[0]+"&file_name="+encodeURIComponent(fileInfo[1]);
a.target = "_blank";
div1.appendChild(a);
}
div2.appendChild(p);
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div1);
return divContainer;
}
function generateFileInfo(fileName, fileSize, p) {
let span = document.createElement("span");
span.textContent = fileName;
p.appendChild(span);
p.appendChild(document.createTextNode(" ("+humanFileSize(fileSize)+")"));
}
function displayChatBottom(speed = undefined) {
let msgBox = document.getElementById("message_box");
let fileTransfer = document.getElementById("file_transfer");
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
msgBox.classList.remove("active");
fileTransfer.classList.remove("active");
} else {
if (session.isContact || session.isOnline) {
msgBox.classList.add("active");
}
if (session.isOnline) {
msgBox.classList.add("online");
} else {
msgBox.classList.remove("online");
}
if (pendingFilesTransfers.has(currentSessionId)) {
let fileInfo = document.getElementById("file_info");
fileInfo.innerHTML = "";
let filesTransfer = pendingFilesTransfers.get(currentSessionId);
let file = filesTransfer.files[filesTransfer.index];
fileInfo.appendChild(document.createTextNode(filesTransfer.index+1+"/"+filesTransfer.files.length+": "));
generateFileInfo(file.name, file.size, fileInfo);
let fileProgress = document.getElementById("file_progress");
fileProgress.style.display = "none"; //hide by default
let fileStatus = document.getElementById("file_status");
fileStatus.removeAttribute("style"); //show by default
let fileCancel = document.getElementById("file_cancel");
fileCancel.style.display = "none"; //hide by default
document.querySelector("#file_progress_bar>div").style.width = 0;
switch (filesTransfer.state) {
case "transferring":
fileCancel.removeAttribute("style"); //show
fileStatus.style.display = "none";
fileProgress.removeAttribute("style"); //show
let percent = (file.transferred/file.size)*100;
document.getElementById("file_percent").textContent = percent.toFixed(2)+"%";
if (typeof speed !== "undefined") {
document.getElementById("file_speed").textContent = humanFileSize(speed)+"/s";
}
document.querySelector("#file_progress_bar>div").style.width = Math.round(percent)+"%";
break;
case "waiting":
fileStatus.textContent = "Waiting for peer confirmation...";
break;
case "aborted":
fileStatus.textContent = "Transfer aborted.";
pendingFilesTransfers.delete(currentSessionId);
break;
case "completed":
fileStatus.textContent = "Transfer completed.";
pendingFilesTransfers.delete(currentSessionId);
}
fileTransfer.classList.add("active");
} else {
fileTransfer.classList.remove("active");
}
}
}
function scrollHistoryToBottom() {
msgLog.scrollTop = msgLog.scrollHeight;
}
function displayHistory(scrollToBottom = true) {
msgLog.innerHTML = "";
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
msgLog.style.display = "none";
} else {
msgLog.style.display = "block";
let previousOutgoing = undefined;
msgHistory.get(currentSessionId).forEach(entry => {
let name = undefined;
let sessionId = undefined;
if (previousOutgoing != entry[0]) {
previousOutgoing = entry[0];
if (entry[0]) { //outgoing msg
name = identityName;
} else {
name = session.name;
sessionId = currentSessionId;
}
}
let div;
if (entry[2]) { //is file
div = generateFile(name, sessionId, entry[0], entry[3]);
} else {
div = generateMessage(name, sessionId, entry[3]);
}
let li = document.createElement("li");
li.appendChild(div);
li.appendChild(generateMessageTimestamp(entry[1]));
msgLog.appendChild(li);
});
if (session.isContact) {
let msgs = pendingMsgs.get(currentSessionId);
if (msgs.length > 0) {
let li = document.createElement("li");
li.classList.add("pending_msgs_divider");
let h4 = document.createElement("h4");
h4.textContent = "Pending messages:";
li.appendChild(h4);
msgLog.appendChild(li);
msgs.forEach(entry => {
let name = undefined;
if (previousOutgoing != true) {
previousOutgoing = true;
name = identityName;
}
let div;
if (entry[0]) { //is file
div = generateFile(name, undefined, true, entry[1]);
} else {
div = generateMessage(name, undefined, entry[1]);
}
let li = document.createElement("li");
li.appendChild(div);
msgLog.appendChild(li);
});
}
}
if (scrollToBottom) {
scrollHistoryToBottom();
}
if (msgLog.scrollHeight <= msgLog.clientHeight && session.isContact) {
socket.send("load_msgs "+currentSessionId);
}
}
}