711 lines
28 KiB
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>
|
|
|