diff --git a/src/frontend/index.css b/src/frontend/index.css new file mode 100644 index 0000000..ae9927b --- /dev/null +++ b/src/frontend/index.css @@ -0,0 +1,329 @@ +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: #52585C; + 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: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + margin: auto; + width: 40%; + box-sizing: border-box; + padding: 20px 70px; + background-color: #2B2F31; + border-radius: 10px; + font-size: 1.3em; +} +.popup_background { + height: 100%; + width: 100%; + position: absolute; + background-color: rgba(0, 0, 0, .5); + z-index: 2; +} +.popup .close { + background-color: unset; + position: absolute; + right: 0; + top: 6px; +} +.popup .close:hover { + background-color: unset; +} +.popup .close::after { + content: url("/static/imgs/icons/cancel"); + background-color: unset; +} +.popup .avatar { + font-size: 4em; + margin: auto; +} +.popup>div>div, .popup>div>button { + font-weight: bold; + display: block; + margin-bottom: 1em; +} +.popup input { + display: block; + margin: 10px; +} +.popup button { + background-color: #52585C; + color: white; + cursor: pointer; + padding: 10px 20px; + border-radius: 8px; + font-weight: bold; +} +.popup button:hover { + background-color: var(--accent); +} +.popup>div>div p { + font-weight: normal; + font-size: 0.9em; +} +.popup h2::before { + content: url("/static/imgs/icons/warning/FF3C00"); + width: 9%; + display: inline-block; + vertical-align: middle; +} +.section_title { + margin-left: 8px; + font-weight: bold; + opacity: 0.5; +} +.section_title:first-of-type { + margin-top: 25px; +} +#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; + cursor: pointer; +} +#me>div { + display: flex; + align-items: center; + flex-grow: 1; +} +#me p { + margin: 0; + font-weight: bold; + display: inline; +} +#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; + flex-grow: 1; +} +#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"); +} +#left_panel ul li .not_seen_marker { + width: .8em; + height: .8em; + background-color: var(--accent); + border-radius: 1em; +} +#refresher { + position: relative; +} +#refresher button { + position: absolute; + right: 10px; + top: 1em; + z-index: 1; +} +#refresher button::after { + content: url("/static/imgs/icons/refresh"); +} +#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"); +} +#chat_header.is_contact #delete_conversation::after { + content: url("/static/imgs/icons/delete_conversation"); +} +#add_contact::after { + content: url("/static/imgs/icons/add_contact"); +} +#chat_header.is_contact #remove_contact::after { + content: url("/static/imgs/icons/remove_contact"); +} +#chat_header.is_contact #verify { + display: unset; +} +#chat_header #verify, +#chat_header.is_verified #verify, +#chat_header.is_contact #add_contact, +#chat_header.offline #add_contact { + display: none; +} +#chat_header.is_contact #verify::after { + content: url("/static/imgs/icons/verified"); +} +#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 a { + color: #238cf5; +} +#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; +} \ No newline at end of file diff --git a/src/frontend/index.html b/src/frontend/index.html index 5e8919e..d77dc1d 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -4,337 +4,7 @@ AIRA - +
@@ -375,694 +45,15 @@
+ - + diff --git a/src/frontend/index.js b/src/frontend/index.js new file mode 100644 index 0000000..897b454 --- /dev/null +++ b/src/frontend/index.js @@ -0,0 +1,671 @@ +"use strict"; + +const ENTER_KEY_CODE = 13; +let identity_name = undefined; +let socket = null; +let notificationAllowed = false; +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; + let session = sessions_data.get(index); + if (session.is_online) { + document.getElementById("message_box").style.display = "flex"; + } + if (!session.seen) { + session.seen = true; + socket.send("set_seen "+index); + } + display_sessions(); + display_header(); + 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); + message_input.value = ""; + } +}); +document.getElementById("delete_conversation").onclick = function() { + let main_div = document.createElement("div"); + main_div.appendChild(generate_popup_warning_title()); + 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) ?"; + main_div.appendChild(p1); + main_div.appendChild(p2); + let button = document.createElement("button"); + button.textContent = "Delete"; + button.onclick = function() { + socket.send("delete_conversation "+current_chat_index); + msg_history.get(current_chat_index).length = 0; + remove_popup(); + display_history(); + } + main_div.appendChild(button); + show_popup(main_div); +} +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(); + display_sessions(); +} +document.getElementById("remove_contact").onclick = function() { + let main_div = document.createElement("div"); + main_div.appendChild(generate_popup_warning_title()); + 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."; + main_div.appendChild(p1); + let p2 = document.createElement("p"); + p2.textContent = "Do you really want to remove this contact ?"; + main_div.appendChild(p2); + let button = document.createElement("button"); + button.textContent = "Delete"; + button.onclick = function() { + socket.send("uncontact "+current_chat_index); + let session = sessions_data.get(current_chat_index); + session.is_contact = false; + session.is_verified = false; + if (!session.is_online) { + sessions_data.delete(current_chat_index); + msg_history.get(current_chat_index).length = 0; + } + display_header(); + display_sessions(); + display_history(); + remove_popup(); + } + main_div.appendChild(button); + show_popup(main_div); +} +document.getElementById("verify").onclick = function() { + socket.send("fingerprints "+current_chat_index); +} +document.getElementById("logout").onclick = function() { + let main_div = document.createElement("div"); + main_div.appendChild(generate_popup_warning_title()); + 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."; + main_div.appendChild(p_warning); + let p_ask = document.createElement("p"); + p_ask.textContent = "Do you really want to log out ?"; + main_div.appendChild(p_ask); + let button = document.createElement("button"); + button.textContent = "Log out"; + button.onclick = logout; + main_div.appendChild(button); + show_popup(main_div); +} +document.getElementById("attach_file").onchange = function(event) { + let file = event.target.files[0]; + if (file.size > 32760000) { + let main_div = document.createElement("main_div"); + main_div.appendChild(generate_popup_warning_title()); + let p = document.createElement("p"); + p.textContent = "The file is too large. Please select files only under 32MB."; + main_div.appendChild(p); + show_popup(main_div); + } else { + let formData = new FormData(); + formData.append("session_id", current_chat_index); + formData.append("", file); + fetch("/send_file", {method: "POST", body: formData}).then(response => { + if (response.ok) { + response.text().then(uuid => on_file_sent(current_chat_index, uuid, file.name)); + } else { + console.log(response); + } + }); + } +} + +//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 div"); +profile_div.onclick = function() { + let main_div = document.createElement("div"); + let avatar = generate_avatar(identity_name); + main_div.appendChild(avatar); + let div_name = document.createElement("div"); + div_name.textContent = "Name:"; + let input_name = document.createElement("input"); + input_name.id = "new_name"; + input_name.type = "text"; + input_name.value = identity_name; + div_name.appendChild(input_name); + let save_name_button = document.createElement("button"); + save_name_button.textContent = "Save"; + save_name_button.onclick = function() { + socket.send("change_name "+document.getElementById("new_name").value); + }; + div_name.appendChild(save_name_button); + main_div.appendChild(div_name); + let div_password = document.createElement("div"); + div_password.textContent = "Change your password:"; + div_password.style.paddingTop = "1em"; + div_password.style.borderTop = "1px solid black"; + if (is_identity_protected) { + let input_old_password = document.createElement("input"); + input_old_password.type = "password"; + input_old_password.placeholder = "Current password"; + div_password.appendChild(input_old_password); + } + let input_password1 = document.createElement("input"); + let input_password2 = document.createElement("input"); + input_password1.type = "password"; + input_password1.placeholder = "New password (empty for no password)"; + input_password2.type = "password"; + input_password2.placeholder = "New password (confirmation)"; + div_password.appendChild(input_password1); + div_password.appendChild(input_password2); + let error_msg = document.createElement("p"); + error_msg.id = "password_error_msg"; + error_msg.style.color = "red"; + div_password.appendChild(error_msg); + let change_password_button = document.createElement("button"); + change_password_button.textContent = "Change password"; + change_password_button.onclick = function() { + let inputs = document.querySelectorAll("input[type=\"password\"]"); + let new_password, new_password_confirm; + if (is_identity_protected) { + new_password = inputs[1]; + new_password_confirm = inputs[2]; + } else { + new_password = inputs[0]; + new_password_confirm = inputs[1]; + } + if (new_password.value == new_password_confirm.value) { + let new_password_set = new_password.value.length > 0; + if (is_identity_protected || new_password_set) { //don't change password if identity is not protected and new password is blank + let msg = "change_password"; + if (is_identity_protected) { + msg += " "+btoa(inputs[0].value); + } + if (new_password_set) { + msg += " "+btoa(new_password.value); + } + socket.send(msg); + } else { + remove_popup(); + } + } else { + new_password.value = ""; + new_password_confirm.value = ""; + error_msg.textContent = "Passwords don't match"; + } + }; + div_password.appendChild(change_password_button); + main_div.appendChild(div_password); + let div_delete = document.createElement("div"); + div_delete.textContent = "Delete identity:"; + div_delete.style.paddingTop = "1em"; + div_delete.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"; + div_delete.appendChild(p); + let delete_button = document.createElement("button"); + delete_button.textContent = "Delete"; + delete_button.style.backgroundColor = "red"; + delete_button.onclick = function() { + let main_div = document.createElement("div"); + main_div.appendChild(generate_popup_warning_title()); + let p = document.createElement("p"); + p.textContent = "This action is irreversible. Are you sure you want to delete all your data ?"; + main_div.appendChild(p); + let delete_button = document.createElement("button"); + delete_button.style.backgroundColor = "red"; + delete_button.textContent = "Delete"; + delete_button.onclick = function() { + socket.send("disappear"); + } + main_div.appendChild(delete_button); + show_popup(main_div); + } + div_delete.appendChild(delete_button); + main_div.appendChild(div_delete); + show_popup(main_div); +} +document.querySelector("#refresher button").onclick = function() { + socket.send("refresh"); +} + +function on_new_session(index, outgoing) { + if (sessions_data.has(index)) { + let session = sessions_data.get(index); + session.is_online = true; + session.outgoing = outgoing; + display_sessions(); + if (current_chat_index == index) { + document.getElementById("message_box").style.display = "flex"; + } + } else { + add_session(index, undefined, outgoing, false, false, true); + } +} +function on_name_told(index, name) { + sessions_data.get(index).name = name; + if (index == current_chat_index) { + display_header(); + } + display_sessions(); +} +function set_not_seen(str_indexes) { + let indexes = str_indexes.split(" "); + for (let i=0; i 0) { + popups[popups.length-1].remove(); + } +} +function generate_popup_warning_title() { + let h2 = document.createElement("h2"); + h2.textContent = "Warning!"; + return h2; +} +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, session) { + let li = document.createElement("li"); + li.setAttribute("data-index", index); + li.appendChild(generate_avatar(session.name)); + li.appendChild(generate_name(session.name)); + if (session.outgoing) { + li.classList.add("outgoing"); + } else { + li.classList.add("incomming"); + } + if (session.is_contact) { + li.classList.add("is_contact"); + } + if (session.is_verified) { + li.classList.add("is_verified"); + } + if (!session.seen) { + let marker = document.createElement("div"); + marker.classList.add("not_seen_marker"); + li.appendChild(marker); + } + if (index == current_chat_index) { + li.classList.add("current"); + } + 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 div1 = document.createElement("div"); + div1.classList.add("file"); + let div2 = document.createElement("div"); + let h4 = document.createElement("h4"); + if (outgoing) { + h4.textContent = "File sent:"; + } else { + h4.textContent = "File received:"; + } + div2.appendChild(h4); + let p = document.createElement("p"); + p.textContent = file_info[1]; + div2.appendChild(p); + div1.appendChild(div2); + let a = document.createElement("a"); + a.href = "/load_file?uuid="+file_info[0]+"&file_name="+encodeURIComponent(file_info[1]); + a.target = "_blank"; + div1.appendChild(a); + let li = document.createElement("li"); + li.appendChild(generate_msg_header(name)); + li.appendChild(div1); + return li; +} +function display_history(scrollToBottom = true) { + 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.appendChild(generate_file(name, entry[0], entry[2])); + } else { + msg_log.appendChild(generate_message(name, entry[2])); + } + }); + if (scrollToBottom) { + msg_log.scrollTop = msg_log.scrollHeight; + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fb78b22..d4c587b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,13 +82,9 @@ fn discover_peers(session_manager: Arc) { }); } -fn load_msgs(session_manager: Arc, ui_connection: &mut UiConnection, session_id: &usize) -> usize { - match session_manager.load_msgs(session_id, constants::MSG_LOADING_COUNT) { - Some(msgs) => { - ui_connection.load_msgs(session_id, &msgs); - msgs.len() - } - None => 0 +fn load_msgs(session_manager: Arc, ui_connection: &mut UiConnection, session_id: &usize) { + if let Some(msgs) = session_manager.load_msgs(session_id, constants::MSG_LOADING_COUNT) { + ui_connection.load_msgs(session_id, &msgs); } } @@ -460,6 +456,8 @@ fn handle_static(req: HttpRequest) -> HttpResponse { if splits[0] == "static" { let mut response_builder = HttpResponse::Ok(); match splits[1] { + "index.js" => return response_builder.content_type(JS_CONTENT_TYPE).body(include_str!("frontend/index.js")), + "index.css" => return response_builder.body(include_str!("frontend/index.css")), "imgs" => { if splits[2] == "icons" && splits.len() <= 5 { let color = if splits.len() == 5 { diff --git a/src/session_manager/mod.rs b/src/session_manager/mod.rs index 340ed75..700d970 100644 --- a/src/session_manager/mod.rs +++ b/src/session_manager/mod.rs @@ -94,6 +94,12 @@ impl SessionManager { } } + fn remove_session(&self, session_id: &usize) { + self.sessions.write().unwrap().remove(session_id); + self.saved_msgs.lock().unwrap().remove(session_id); + self.not_seen.write().unwrap().retain(|x| x != session_id); + } + fn handle_new_session(session_manager: Arc, mut session: Session, outgoing: bool) { tokio::spawn(async move { let mut peer_public_key = [0; PUBLIC_KEY_LENGTH]; @@ -131,12 +137,12 @@ impl SessionManager { let mut sessions = session_manager.sessions.write().unwrap(); let mut is_new_session = true; for (_, registered_session) in sessions.iter() { - if registered_session.1 == peer_public_key { //already connected with a different addr + if registered_session.1 == peer_public_key { //already connected to this identity is_new_session = false; break; } } - if is_new_session { + if is_new_session && session_manager.is_identity_loaded() { //check if we didn't logged out during the handshake let (sender, receiver) = mpsc::channel(32); let mut session_id = None; for (i, contact) in session_manager.loaded_contacts.read().unwrap().iter() { @@ -144,6 +150,7 @@ impl SessionManager { sessions.insert(*i, (outgoing, peer_public_key, sender.clone())); is_contact = true; session_id = Some(*i); + break; } } if session_id.is_none() { //if not a contact, increment the session_counter @@ -169,6 +176,7 @@ impl SessionManager { Ok(_) => {} Err(e) => { print_error!(e); + session_manager.remove_session(&session_id); return; } } @@ -268,9 +276,7 @@ impl SessionManager { } } } - session_manager.sessions.write().unwrap().remove(&session_id); - session_manager.saved_msgs.lock().unwrap().remove(&session_id); - session_manager.not_seen.write().unwrap().retain(|x| *x != session_id); + session_manager.remove_session(&session_id); } } });