AIRA/src/frontend/index.html

711 lines
28 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AIRA</title>
<link rel="stylesheet" href="/static/commons/style.css">
<style>
:root {
--accent: #FF3C00;
--transparent: #00000000;
}
html {
height: 100%;
font-family: Arial, Helvetica, Sans-Serif;
color: white;
}
body {
margin: 0;
height: 100%;
}
main {
display: grid;
grid-template-columns: 25% auto;
height: 100%;
}
.panel {
padding-left: 20px;
padding-right: 20px;
display: flex;
flex-direction: column;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
button {
background-color: var(--transparent);
border: none;
cursor: pointer;
}
button::after {
background-color: #8E99A4;
border-radius: 100%;
display: block;
width: 20px;
height: 20px;
padding: 8px;
}
button:hover::after {
background-color: var(--accent);
}
input[type="file"] {
display: none;
}
.file_picker {
display: flex;
align-items: center;
cursor: pointer;
}
.file_picker::after {
content: url("/static/imgs/icons/attach/FF3C00");
width: 2em;
}
.popup {
position: fixed;
top: 40%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
width: 40%;
box-sizing: border-box;
padding: 20px 70px;
background-color: #2B2F31;
border-radius: 10px;
text-align: center;
font-size: 1.3em;
}
.popup h2::before {
content: url("/static/imgs/icons/warning/FF3C00");
width: 9%;
display: inline-block;
vertical-align: middle;
}
.popup .buttons {
display: flex;
justify-content: center;
}
.popup button {
background-color: #52585C;
color: white;
cursor: pointer;
padding: 15px 30px;
border-radius: 8px;
margin-left: 25px;
margin-right: 25px;
font-weight: bold;
}
.popup button:hover {
background-color: var(--accent);
}
.section_title {
margin-left: 8px;
font-weight: bold;
opacity: 0.5;
}
#left_panel {
background-color: #1D2228;
}
#right_panel {
background-color: #15191E;
display: flex;
flex-direction: column;
overflow: hidden;
}
#me {
border-bottom: 2px solid var(--accent);
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
}
#me p {
flex-grow: 1;
margin: 0;
font-weight: bold;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
}
#left_panel ul li {
font-size: 1.1em;
padding: 15px;
height: 50px;
box-sizing: border-box;
margin-left: 8px;
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
#left_panel ul li>p {
display: inline;
font-weight: bold;
}
#left_panel ul li .avatar {
width: 2em;
height: 2em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
}
#left_panel ul li.output {
}
#left_panel ul li.input {
}
#left_panel ul li p::after {
content: url("/static/imgs/icons/warning/FF3C00");
display: inline-block;
width: 1em;
margin-left: .3em;
vertical-align: middle;
}
#left_panel ul li.is_contact p::after {
content: unset;
}
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/FF3C00");
}
#connect_box {
margin-bottom: 20px;
}
#chat_header {
flex-direction: row;
align-items: center;
padding: 5px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
align-items: center;
flex-grow: 1;
}
#chat_header>div>p {
font-weight: bold;
}
#chat_header p::after {
content: url("/static/imgs/icons/warning/FF3C00");
display: inline-block;
width: 1.2em;
vertical-align: middle;
padding-left: .3em;
}
#chat_header.is_contact p::after {
content: unset;
}
#chat_header.is_verified p::after {
content: url("/static/imgs/icons/verified/FF3C00");
}
#add_contact::after {
content: url("/static/imgs/icons/add_contact");
}
#remove_contact::after {
content: url("/static/imgs/icons/remove_contact");
}
#chat_header.is_contact #verify::after {
content: url("/static/imgs/icons/verified");
}
#chat_header.is_verified #verify::after {
content: unset;
}
#logout::after {
content: url("/static/imgs/icons/logout");
}
#message_input {
border: unset;
padding: 1em;
font-size: 1.1em;
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
}
#msg_log li>div:first-of-type { /*Message header*/
display: flex;
align-items: center;
}
#msg_log p.name {
margin: 0;
color: var(--accent);
font-weight: bold;
}
#msg_log li>div:last-of-type { /*Message content container*/
margin-left: 2em;
}
#msg_log li p {
margin-top: 0;
}
#msg_log .file {
display: flex;
align-items: end;
margin-bottom: 1em;
border-left: 3px solid var(--accent);
padding-left: .5em;
margin-top: .5em;
}
#msg_log .file div { /*Title and filename container*/
display: flex;
flex-direction: column;
}
#msg_log .file h4 {
margin: 0;
}
#msg_log .file p {
margin: 0;
color: var(--accent);
}
#msg_log .file a::after {
content: url("/static/imgs/icons/download/FF3C00");
display: block;
width: 2em;
margin-left: 1em;
}
#message_box, #chat_header, #msg_log {
display: none;
}
</style>
</head>
<body>
<main id="main">
<div id="left_panel" class="panel">
<div id="me">
<p>IDENTITY_NAME</p>
<button id="logout" title="Log out"></button>
</div>
<p class="section_title">Online peers:</p>
<ul id="online_sessions">
</ul>
<p class="section_title">Offline contacts:</p>
<ul id="offline_sessions">
</ul>
<div id="connect_box">
<p class="section_title">Add a new peer by IP:</p>
<input type="text" id="ip_input" placeholder="Enter IP address">
</div>
</div>
<div id="right_panel" class="panel">
<div id="chat_header">
<div></div>
<button id="verify" title="Verify"></button>
<button id="add_contact" title="Add to contact"></button>
<button id="remove_contact" title="Remove from contact"></button>
</div>
<ul id="msg_log">
</ul>
<div id="message_box">
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file">
</label>
</div>
</div>
</main>
<script src="/static/libs/linkify.min.js"></script>
<script src="/static/libs/linkify-element.min.js"></script>
<script src="/static/commons/script.js"></script>
<script>
"use strict";
const ENTER_KEY_CODE = 13;
const identity_name = "IDENTITY_NAME";
let socket = null;
let current_chat_index = -1;
let sessions_data = new Map();
let msg_history = new Map();
function on_click_session(event) {
let index = event.currentTarget.getAttribute("data-index");
if (index != null){
current_chat_index = index;
refresh_sessions();
display_header();
if (sessions_data.get(index).is_online){
document.getElementById("message_box").style.display = "flex";
}
display_history();
}
}
let ip_input = document.getElementById("ip_input");
ip_input.addEventListener("keyup", function(event){
if (event.keyCode === ENTER_KEY_CODE) {
socket.send("connect "+ip_input.value);
ip_input.value = "";
}
});
let message_input = document.getElementById("message_input");
message_input.addEventListener("keyup", function(event){
if (event.keyCode === ENTER_KEY_CODE) {
socket.send("send "+current_chat_index+" "+message_input.value);
msg_history.get(current_chat_index).push([true, false, message_input.value]);
message_input.value = "";
display_history(current_chat_index);
}
});
document.getElementById("add_contact").onclick = function(){
socket.send("contact "+current_chat_index+" "+sessions_data.get(current_chat_index).name);
sessions_data.get(current_chat_index).is_contact = true;
display_header();
refresh_sessions();
}
document.getElementById("remove_contact").onclick = function(){
socket.send("uncontact "+current_chat_index);
let session = sessions_data.get(current_chat_index);
session.is_contact = false;
session.is_verified = false;
display_header();
refresh_sessions();
}
document.getElementById("verify").onclick = function(){
socket.send("fingerprints "+current_chat_index);
}
document.getElementById("logout").onclick = function(){
display_popup("<p>If you log out, you will no longer receive messages and pending messages will not be sent until you log in back.</p><p>Do you really want to log out ?</p>",
"<button onclick=\"logout();\">Log out</button>"
);
}
document.getElementById("attach_file").onchange = function(event){
let file = event.target.files[0];
if (file.size > 45000000) { //45MB
display_popup("<p>The file is too large. Please select only files under 45MB.</p>");
} else {
let reader = new FileReader();
reader.onload = function(e){
socket.send("file "+current_chat_index+" "+file.name);
let data = e.target.result;
socket.send(data);
}
reader.readAsArrayBuffer(file);
}
}
socket = new WebSocket("ws://"+location.hostname+":WEBSOCKET_PORT/ws");
socket.onopen = function() {
console.log("Connected");
};
socket.onmessage = function(msg){
if (typeof msg.data == "string"){
console.log("Message: "+msg.data);
let args = msg.data.split(" ");
switch (args[0]){
case "connected":
on_connected(args[1]);
break;
case "disconnected":
on_disconnected(args[1]);
break;
case "new_session":
on_new_session(args[1]);
break;
case "new_message":
on_message_received(args[1], args.slice(2).join(" "));
break;
case "load_sent_msg":
on_msg_load(args[1], args.slice(2).join(" "));
break;
case "load_sent_file":
on_file_load(args[1], args[2], args.slice(3).join(" "));
break;
case "name_told":
on_name_told(args[1], args.slice(2).join(" "));
break;
case "is_contact":
on_is_contact(args[1], args[2], args.slice(3).join(" "));
break;
case "fingerprints":
on_fingerprints(args[1], args[2]);
break;
case "file":
on_file_received(args[1], args[2], args.slice(3).join(" "));
break;
case "file_sent":
on_file_sent(args[1], args[2], args.slice(3).join(" "));
break;
}
} else { //receiving file
on_file_re
}
}
socket.onclose = function(){
console.log("Disconnected");
}
let me = document.getElementById("me");
me.insertBefore(generate_avatar(identity_name), me.firstChild);
function on_connected(index){
if (sessions_data.has(index)){
sessions_data.get(index).is_online = true;
refresh_sessions();
} else {
add_session(index, undefined, true, false, false, true);
}
}
function on_new_session(index){
if (sessions_data.has(index)){
let session = sessions_data.get(index);
session.is_online = true;
session.outgoing = false;
refresh_sessions();
} else {
add_session(index, undefined, false, false, false, true);
}
}
function on_name_told(index, name){
sessions_data.get(index).name = name;
if (index == current_chat_index) {
display_header();
}
refresh_sessions();
}
function on_is_contact(index, str_verified, name){
let verified = (str_verified === "true");
if (sessions_data.has(index)){
let session = sessions_data.get(index);
session.is_contact = true;
session.is_verified = verified;
on_name_told(index, name);
} else {
add_session(index, name, true, true, verified, false);
}
}
function on_message_received(index, msg){
msg_history.get(index).push([false, false, msg]);
if (current_chat_index == index){
display_history();
}
}
function on_msg_load(index, msg){
msg_history.get(index).push([true, false, msg]);
if (current_chat_index == index){
display_history();
}
}
function on_file_load(index, uuid, file_name){
msg_history.get(index).push([true, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function on_disconnected(index){
if (current_chat_index == index){
document.getElementById("message_box").style.display = "none";
}
let session = sessions_data.get(index);
if (session.is_contact){
session.is_online = false;
} else {
sessions_data.delete(index);
if (current_chat_index == index){
current_chat_index = -1;
}
}
refresh_sessions();
}
function on_fingerprints(local, peer){
let beautify_fingerprints = function(f){
for (let i=4; i<f.length; i+=5){
f = f.slice(0, i)+" "+f.slice(i);
}
return f;
};
display_popup("<p>Compare the following fingerprints by a trusted way of communication (such as real life) before clicking on Verify.</p><p>Local fingerprint:</p><pre>"+beautify_fingerprints(local)+"</pre><p>Peer fingerprint:</p><pre>"+beautify_fingerprints(peer)+"</pre>",
"<button onclick=\"verify();\">Verified</button>"
);
}
function on_file_received(index, uuid, file_name){
msg_history.get(index).push([false, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function on_file_sent(index, uuid, file_name){
msg_history.get(index).push([true, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function add_session(index, name, outgoing, is_contact, is_verified, is_online){
sessions_data.set(index, {
"name": name,
"outgoing": outgoing,
"is_contact": is_contact,
"is_verified": is_verified,
"is_online": is_online,
});
msg_history.set(index, []);
refresh_sessions();
}
function refresh_sessions(){
let online_sessions = document.getElementById("online_sessions");
online_sessions.innerHTML = "";
let offline_sessions = document.getElementById("offline_sessions");
offline_sessions.innerHTML = "";
sessions_data.forEach(function (session, index){
let session_element = generate_session(index, session.outgoing, session.is_contact, session.is_verified, session.name);
if (session.is_online){
online_sessions.appendChild(session_element);
} else {
offline_sessions.appendChild(session_element) ;
}
});
}
function verify(){
socket.send("verify "+current_chat_index);
sessions_data.get(current_chat_index).is_verified = true;
remove_popup();
display_header();
refresh_sessions();
}
function logout(){
window.location = "/logout";
}
function display_header(){
let chat_header = document.getElementById("chat_header");
let session = sessions_data.get(current_chat_index);
chat_header.children[0].innerHTML = "";
chat_header.children[0].appendChild(generate_avatar(session.name));
chat_header.children[0].appendChild(generate_name(session.name));
chat_header.style.display = "flex";
if (session.is_contact){ //is_contact
chat_header.classList.add("is_contact");
document.getElementById("add_contact").style.display = "none";
document.getElementById("remove_contact").style.display = "block";
} else {
chat_header.classList.remove("is_contact");
document.getElementById("remove_contact").style.display = "none";
document.getElementById("add_contact").style.display = "block";
}
if (session.is_verified){
chat_header.classList.add("is_verified");
} else {
chat_header.classList.remove("is_verified");
}
}
function display_popup(content, button){
let popup_content = "<h2>Warning!</h2>"+content+"<div class=\"buttons\"><button onclick=\"remove_popup();\">";
if (typeof button == "undefined"){
popup_content += "OK</button></div>";
} else {
popup_content += "Cancel</button>"+button+"</div>";
}
let popup = document.createElement("div");
popup.classList.add("popup");
popup.innerHTML = popup_content;
let main = document.getElementsByTagName("main")[0];
main.insertBefore(popup, main.firstChild);
}
function remove_popup(){
document.getElementsByClassName("popup")[0].remove();
}
function generate_name(name){
let p = document.createElement("p");
if (typeof name == "undefined"){
p.appendChild(document.createTextNode("Unknown"));
} else {
p.appendChild(document.createTextNode(name));
}
return p;
}
function generate_session(index, outgoing, is_contact, is_verified, name){
let li = document.createElement("li");
li.setAttribute("data-index", index);
if (outgoing) {
li.classList.add("outgoing");
} else {
li.classList.add("incomming");
}
if (is_contact) {
li.classList.add("is_contact");
}
if (is_verified) {
li.classList.add("is_verified");
}
if (index == current_chat_index){
li.classList.add("current");
}
li.appendChild(generate_avatar(name));
li.appendChild(generate_name(name));
li.onclick = on_click_session;
return li;
}
function generate_msg_header(name){
let text = document.createTextNode(name);
let p = document.createElement("p");
p.appendChild(text);
p.classList.add("name");
let div = document.createElement("div");
div.appendChild(generate_avatar(name));
div.appendChild(p);
return div;
}
function generate_message(name, msg){
let text = document.createTextNode(msg);
let p = document.createElement("p");
p.appendChild(text);
let div = document.createElement("div");
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
li.appendChild(generate_msg_header(name))
li.appendChild(div);
return li;
}
function generate_file(name, outgoing, file_info){
let content = "<li>"+generate_msg_header(name)+"<div class=\"file\"><div><h4>";
if (outgoing) {
content += "File sent:";
} else {
content += "File received:";
}
content += "</h4><p>"+file_info[1]+"</p></div>";
if (file_info[0] !== "None"){
content += "<a href=\"/load_file?uuid="+file_info[0]+"&file_name="+encodeURIComponent(file_info[1])+"\" target=\"_blank\"></a>";
}
content += "</div></li>";
return content;
}
function display_history(){
let msg_log = document.getElementById("msg_log");
msg_log.style.display = "block";
msg_log.innerHTML = "";
msg_history.get(current_chat_index).forEach(entry => {
let name;
if (entry[0]){ //outgoing msg
name = identity_name;
} else {
name = sessions_data.get(current_chat_index).name;
}
if (entry[1]){ //is file
msg_log.innerHTML += generate_file(name, entry[0], entry[2]);
} else {
msg_log.appendChild(generate_message(name, entry[2]));
}
});
msg_log.scrollTop = msg_log.scrollHeight;
}
function get_session_li_by_index(index){
["online_sessions", "offline_sessions"].forEach(function(id){
let sessions = document.getElementById(id);
for (let i = 0; i < sessions.children.length; i++) {
let li = sessions.children[i];
if (li.getAttribute("data-index") == index){
return li;
}
}
});
}
</script>
</body>
</html>