diff --git a/onionshare/web/chat_mode.py b/onionshare/web/chat_mode.py index 05014a22..ed946a14 100644 --- a/onionshare/web/chat_mode.py +++ b/onionshare/web/chat_mode.py @@ -1,4 +1,12 @@ -from flask import Request, request, render_template, make_response, flash, redirect, session +from flask import ( + Request, + request, + render_template, + make_response, + flash, + redirect, + session, +) from werkzeug.utils import secure_filename from flask_socketio import emit, join_room, leave_room @@ -14,8 +22,8 @@ class ChatModeWeb: self.web = web - self.can_upload = True - self.uploads_in_progress = [] + # This tracks users in the room + self.connected_users = [] # This tracks the history id self.cur_history_id = 0 @@ -31,11 +39,14 @@ class ChatModeWeb: def index(): history_id = self.cur_history_id self.cur_history_id += 1 - session["name"] = self.common.build_username() + session["name"] = ( + session.get("name") + if session.get("name") + else self.common.build_username() + ) session["room"] = self.web.settings.default_settings["chat"]["room"] self.web.add_request( - request.path, - {"id": history_id, "status_code": 200}, + request.path, {"id": history_id, "status_code": 200}, ) self.web.add_request(self.web.REQUEST_LOAD, request.path) @@ -43,7 +54,7 @@ class ChatModeWeb: render_template( "chat.html", static_url_path=self.web.static_url_path, - username=session.get("name") + username=session.get("name"), ) ) return self.web.add_security_headers(r) @@ -52,16 +63,16 @@ class ChatModeWeb: def joined(message): """Sent by clients when they enter a room. A status message is broadcast to all people in the room.""" - session["worker"] = UserListWorker(self.web.socketio) - session["thread"] = self.web.socketio.start_background_task( - session["worker"].background_thread, session["name"] - ) + self.connected_users.append(session.get("name")) join_room(session.get("room")) emit( "status", - {"msg": session.get("name") + " has entered the room.", - "user": session.get("name")}, - room=session.get("room") + { + "msg": "{} has joined.".format(session.get("name")), + "connected_users": self.connected_users, + "user": session.get("name"), + }, + room=session.get("room"), ) @self.web.socketio.on("text", namespace="/chat") @@ -70,8 +81,8 @@ class ChatModeWeb: The message is sent to all people in the room.""" emit( "message", - {"msg": session.get("name") + ": " + message["msg"]}, - room=session.get("room") + {"msg": "{}: {}".format(session.get("name"), message["msg"])}, + room=session.get("room"), ) @self.web.socketio.on("update_username", namespace="/chat") @@ -80,40 +91,33 @@ class ChatModeWeb: The message is sent to all people in the room.""" current_name = session.get("name") session["name"] = message["username"] - session["worker"].stop_thread() - session["worker"] = UserListWorker(self.web.socketio) - session['thread'] = self.web.socketio.start_background_task( - session["worker"].background_thread, session['name'] - ) + self.connected_users[ + self.connected_users.index(current_name) + ] = session.get("name") emit( "status", - {"msg": current_name + " has updated their username to: " + session.get("name"), - "old_name": current_name, - "new_name": session.get("name") + { + "msg": "{} has updated their username to: {}".format( + current_name, session.get("name") + ), + "connected_users": self.connected_users, + "old_name": current_name, + "new_name": session.get("name"), }, - room=session.get("room") + room=session.get("room"), ) - - -class UserListWorker(object): - - def __init__(self, socketio): - """ - assign socketio object to emit - """ - self.socketio = socketio - self.switch = True - - def background_thread(self, name): - count = 0 - while self.switch: - self.socketio.sleep(5) - count += 1 - self.socketio.emit('update_list', - {'name': name, 'count': count}, - namespace="/chat", - broadcast=True) - - def stop_thread(self): - self.switch = False + @self.web.socketio.on("disconnect", namespace="/chat") + def disconnect(): + """Sent by clients when they disconnect from a room. + A status message is broadcast to all people in the room.""" + self.connected_users.remove(session.get("name")) + leave_room(session.get("room")) + emit( + "status", + { + "msg": "{} has left the room.".format(session.get("name")), + "connected_users": self.connected_users, + }, + room=session.get("room"), + ) diff --git a/share/static/css/style.css b/share/static/css/style.css index bb000ca4..6eea11ca 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -144,6 +144,7 @@ table.file-list td:last-child { .chat-users .editable-username { display: flex; + padding: 1rem; } .chat-users input#username { @@ -179,6 +180,16 @@ table.file-list td:last-child { height: 100%; } +@media (max-width: 992px) { + .chat-users .editable-username { + display: block; + } + + .chat-users input#username { + width: 90%; + } +} + .upload-wrapper { align-items: center; justify-content: center; diff --git a/share/static/js/chat.js b/share/static/js/chat.js index c3bcf4b8..7cbf9ab6 100644 --- a/share/static/js/chat.js +++ b/share/static/js/chat.js @@ -1,65 +1,123 @@ $(function(){ - var socket; - var last_username; - var username_list = []; $(document).ready(function(){ - socket = io.connect('http://' + document.domain + ':' + location.port + '/chat'); + var socket = io.connect('http://' + document.domain + ':' + location.port + '/chat'); + + // Store current username received from app context var current_username = $('#username').val(); + + // On browser connect, emit a socket event to be added to + // room and assigned random username socket.on('connect', function() { socket.emit('joined', {}); }); + + // Triggered on any status change by any user, such as some + // user joined, or changed username, or left, etc. socket.on('status', function(data) { - $('#chat').append('

' + sanitizeHTML(data.msg) + '

'); - if (data.user && current_username !== data.user) { - $('#user-list').append('
  • ' + sanitizeHTML(data.user) + '
  • ') - username_list.push(data.user); - } - if (data.new_name && current_username !== data.new_name) { - last_username = current_username; - current_username = data.new_name; - username_list[username_list.indexOf(last_username)] = current_username; - $('#user-list li').each(function(key, value) { - if ($(value).text() === data.old_name) { - $(value).html(sanitizeHTML(current_username)); - } - }) - } - $('#chat').scrollTop($('#chat')[0].scrollHeight); - }); - socket.on('update_list', function(data) { - if (username_list.indexOf(data.name) === -1 && - current_username !== data.name && - last_username !== data.name - ) { - username_list.push(data.name); - $('#user-list').append('
  • ' + sanitizeHTML(data.name) + '
  • ') - } - $('#chat').scrollTop($('#chat')[0].scrollHeight); + addMessageToRoom(data, current_username, 'status'); }); + + // Triggered when message is received from a user. Even when sent + // by self, it get triggered after the server sends back the emit. socket.on('message', function(data) { - $('#chat').append('

    ' + sanitizeHTML(data.msg) + '

    '); - $('#chat').scrollTop($('#chat')[0].scrollHeight); + addMessageToRoom(data, current_username, 'chat'); }); + + // Trigger new message on enter or click of send message button. $('#new-message').on('keypress', function(e) { var code = e.keyCode || e.which; if (code == 13) { emitMessage(socket); } }); - $('#send-button').on('click', emitMessage); + $('#send-button').on('click', function(e) { + emitMessage(socket); + }); + + // Update username $('#update-username').on('click', function() { var username = $('#username').val(); + current_username = username; socket.emit('update_username', {username: username}); }); + + // Show warning of losing data + $(window).on('beforeunload', function (e) { + e.preventDefault(); + e.returnValue = ''; + return ''; + }); }); }); +var addMessageToRoom = function(data, current_username, messageType) { + var scrollDiff = getScrollDiffBefore(); + if (messageType === 'status') { + addStatusMessage(data.msg); + if (data.connected_users) { + addUserList(data.connected_users, current_username); + } + } else if (messageType === 'chat') { + addChatMessage(data.msg) + } + scrollBottomMaybe(scrollDiff); +} + var emitMessage = function(socket) { var text = $('#new-message').val(); $('#new-message').val(''); + $('#chat').scrollTop($('#chat')[0].scrollHeight); socket.emit('text', {msg: text}); } +/************************************/ +/********* Util Functions ***********/ +/************************************/ + +var createUserListHTML = function(connected_users, current_user) { + var userListHTML = ''; + connected_users.sort(); + connected_users.forEach(function(username) { + if (username !== current_user) { + userListHTML += `
  • ${sanitizeHTML(username)}
  • `; + } + }); + return userListHTML; +} + +var getScrollDiffBefore = function() { + return $('#chat').scrollTop() - ($('#chat')[0].scrollHeight - $('#chat')[0].offsetHeight); +} + +var scrollBottomMaybe = function(scrollDiff) { + // Scrolls to bottom if the user is scrolled at bottom + // if the user has scrolled upp, it wont scroll at bottom. + // Note: when a user themselves send a message, it will still + // scroll to the bottom even if they had scrolled up before. + if (scrollDiff > 0) { + $('#chat').scrollTop($('#chat')[0].scrollHeight); + } +} + +var addStatusMessage = function(message) { + $('#chat').append( + `

    ${sanitizeHTML(message)}

    ` + ); +} + +var addChatMessage = function(message) { + $('#chat').append(`

    ${sanitizeHTML(message)}

    `); +} + +var addUserList = function(connected_users, current_username) { + $('#user-list').html( + createUserListHTML( + connected_users, + current_username + ) + ); +} + var sanitizeHTML = function(str) { var temp = document.createElement('span'); temp.textContent = str; diff --git a/share/templates/chat.html b/share/templates/chat.html index 4bcf298f..6eb5eeb6 100644 --- a/share/templates/chat.html +++ b/share/templates/chat.html @@ -26,11 +26,11 @@
    +
    + + +