Refactors logic for chat user list and scroll

- Refactors server side code to use instance variable instead of
background thread to generate a list of connected users
- Send this user list anytime any change is made to the list. It can
be: join, update username, disconnect
- In js, render the entire user list everytime it is received.
- Scroll to the bottom of the chat, everytime the current user
sends a message
- Else, if already at the bottom of the chat, scroll to the bottom
after appending incoming status or chat message. But if the user
is scrolled up in the chat window, then do not scroll to the bottom
- When refreshed or close tab is clicked, default browser warning is
shown.
- On receiving disconnect, the browser removes user from room.
- If refreshed, it is shown as if the user left and joined again.
This commit is contained in:
Saptak S 2020-05-04 03:47:13 +05:30
parent 440c0b0ab1
commit 89b112ea95
No known key found for this signature in database
GPG key ID: 2D9B32E54C68A3FB
4 changed files with 158 additions and 85 deletions

View file

@ -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 werkzeug.utils import secure_filename
from flask_socketio import emit, join_room, leave_room from flask_socketio import emit, join_room, leave_room
@ -14,8 +22,8 @@ class ChatModeWeb:
self.web = web self.web = web
self.can_upload = True # This tracks users in the room
self.uploads_in_progress = [] self.connected_users = []
# This tracks the history id # This tracks the history id
self.cur_history_id = 0 self.cur_history_id = 0
@ -31,11 +39,14 @@ class ChatModeWeb:
def index(): def index():
history_id = self.cur_history_id history_id = self.cur_history_id
self.cur_history_id += 1 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"] session["room"] = self.web.settings.default_settings["chat"]["room"]
self.web.add_request( self.web.add_request(
request.path, request.path, {"id": history_id, "status_code": 200},
{"id": history_id, "status_code": 200},
) )
self.web.add_request(self.web.REQUEST_LOAD, request.path) self.web.add_request(self.web.REQUEST_LOAD, request.path)
@ -43,7 +54,7 @@ class ChatModeWeb:
render_template( render_template(
"chat.html", "chat.html",
static_url_path=self.web.static_url_path, static_url_path=self.web.static_url_path,
username=session.get("name") username=session.get("name"),
) )
) )
return self.web.add_security_headers(r) return self.web.add_security_headers(r)
@ -52,16 +63,16 @@ class ChatModeWeb:
def joined(message): def joined(message):
"""Sent by clients when they enter a room. """Sent by clients when they enter a room.
A status message is broadcast to all people in the room.""" A status message is broadcast to all people in the room."""
session["worker"] = UserListWorker(self.web.socketio) self.connected_users.append(session.get("name"))
session["thread"] = self.web.socketio.start_background_task(
session["worker"].background_thread, session["name"]
)
join_room(session.get("room")) join_room(session.get("room"))
emit( emit(
"status", "status",
{"msg": session.get("name") + " has entered the room.", {
"user": session.get("name")}, "msg": "{} has joined.".format(session.get("name")),
room=session.get("room") "connected_users": self.connected_users,
"user": session.get("name"),
},
room=session.get("room"),
) )
@self.web.socketio.on("text", namespace="/chat") @self.web.socketio.on("text", namespace="/chat")
@ -70,8 +81,8 @@ class ChatModeWeb:
The message is sent to all people in the room.""" The message is sent to all people in the room."""
emit( emit(
"message", "message",
{"msg": session.get("name") + ": " + message["msg"]}, {"msg": "{}: {}".format(session.get("name"), message["msg"])},
room=session.get("room") room=session.get("room"),
) )
@self.web.socketio.on("update_username", namespace="/chat") @self.web.socketio.on("update_username", namespace="/chat")
@ -80,40 +91,33 @@ class ChatModeWeb:
The message is sent to all people in the room.""" The message is sent to all people in the room."""
current_name = session.get("name") current_name = session.get("name")
session["name"] = message["username"] session["name"] = message["username"]
session["worker"].stop_thread() self.connected_users[
session["worker"] = UserListWorker(self.web.socketio) self.connected_users.index(current_name)
session['thread'] = self.web.socketio.start_background_task( ] = session.get("name")
session["worker"].background_thread, session['name']
)
emit( emit(
"status", "status",
{"msg": current_name + " has updated their username to: " + session.get("name"), {
"old_name": current_name, "msg": "{} has updated their username to: {}".format(
"new_name": session.get("name") 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"),
) )
@self.web.socketio.on("disconnect", namespace="/chat")
def disconnect():
class UserListWorker(object): """Sent by clients when they disconnect from a room.
A status message is broadcast to all people in the room."""
def __init__(self, socketio): self.connected_users.remove(session.get("name"))
""" leave_room(session.get("room"))
assign socketio object to emit emit(
""" "status",
self.socketio = socketio {
self.switch = True "msg": "{} has left the room.".format(session.get("name")),
"connected_users": self.connected_users,
def background_thread(self, name): },
count = 0 room=session.get("room"),
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

View file

@ -144,6 +144,7 @@ table.file-list td:last-child {
.chat-users .editable-username { .chat-users .editable-username {
display: flex; display: flex;
padding: 1rem;
} }
.chat-users input#username { .chat-users input#username {
@ -179,6 +180,16 @@ table.file-list td:last-child {
height: 100%; height: 100%;
} }
@media (max-width: 992px) {
.chat-users .editable-username {
display: block;
}
.chat-users input#username {
width: 90%;
}
}
.upload-wrapper { .upload-wrapper {
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -1,65 +1,123 @@
$(function(){ $(function(){
var socket;
var last_username;
var username_list = [];
$(document).ready(function(){ $(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(); 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.on('connect', function() {
socket.emit('joined', {}); 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) { socket.on('status', function(data) {
$('#chat').append('<p><small><i>' + sanitizeHTML(data.msg) + '</i></small></p>'); addMessageToRoom(data, current_username, 'status');
if (data.user && current_username !== data.user) {
$('#user-list').append('<li>' + sanitizeHTML(data.user) + '</li>')
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('<li>' + sanitizeHTML(data.name) + '</li>')
}
$('#chat').scrollTop($('#chat')[0].scrollHeight);
}); });
// 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) { socket.on('message', function(data) {
$('#chat').append('<p>' + sanitizeHTML(data.msg) + '</p>'); addMessageToRoom(data, current_username, 'chat');
$('#chat').scrollTop($('#chat')[0].scrollHeight);
}); });
// Trigger new message on enter or click of send message button.
$('#new-message').on('keypress', function(e) { $('#new-message').on('keypress', function(e) {
var code = e.keyCode || e.which; var code = e.keyCode || e.which;
if (code == 13) { if (code == 13) {
emitMessage(socket); emitMessage(socket);
} }
}); });
$('#send-button').on('click', emitMessage); $('#send-button').on('click', function(e) {
emitMessage(socket);
});
// Update username
$('#update-username').on('click', function() { $('#update-username').on('click', function() {
var username = $('#username').val(); var username = $('#username').val();
current_username = username;
socket.emit('update_username', {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 emitMessage = function(socket) {
var text = $('#new-message').val(); var text = $('#new-message').val();
$('#new-message').val(''); $('#new-message').val('');
$('#chat').scrollTop($('#chat')[0].scrollHeight);
socket.emit('text', {msg: text}); 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 += `<li>${sanitizeHTML(username)}</li>`;
}
});
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(
`<p><small><i>${sanitizeHTML(message)}</i></small></p>`
);
}
var addChatMessage = function(message) {
$('#chat').append(`<p>${sanitizeHTML(message)}</p>`);
}
var addUserList = function(connected_users, current_username) {
$('#user-list').html(
createUserListHTML(
connected_users,
current_username
)
);
}
var sanitizeHTML = function(str) { var sanitizeHTML = function(str) {
var temp = document.createElement('span'); var temp = document.createElement('span');
temp.textContent = str; temp.textContent = str;

View file

@ -26,11 +26,11 @@
<div class="chat-container"> <div class="chat-container">
<div class="chat-users"> <div class="chat-users">
<div class="editable-username">
<input id="username" value="{{ username }}" />
<button id="update-username">Save</button>
</div>
<ul id="user-list"> <ul id="user-list">
<li class="editable-username">
<input id="username" value="{{ username }}" />
<button id="update-username">Save</button>
</li>
</ul> </ul>
</div> </div>
<div class="chat-wrapper"> <div class="chat-wrapper">