This is the documentation of my project2 in which I show in detail the features of my project of book reviews website.
Below I will explain each of the requirements and the files that meet them.
Display Name: When a user visits your web application for the first time, they should be prompted to type in a display name that will eventually be associated with every message the user sends. If a user closes the page and returns to your app later, the display name should still be remembered.
Channel Creation: Any user should be able to create a new channel, so long as its name doesn’t conflict with the name of an existing channel.
Channel List: Users should be able to see a list of all current channels, and selecting one should allow the user to view the channel. We leave it to you to decide how to display such a list.
Messages View: Once a channel is selected, the user should see any messages that have already been sent in that channel, up to a maximum of 100 messages. Your app should only store the 100 most recent messages per channel in server-side memory.
Sending Messages: Once in a channel, users should be able to send text messages to others the channel. When a user sends a message, their display name and the timestamp of the message should be associated with the message. All users in the channel should then see the new message (with display name and timestamp) appear on their channel page. Sending and receiving messages should NOT require reloading the page.
Remembering the Channel: If a user is on a channel page, closes the web browser window, and goes back to your web application, your application should remember what channel the user was on previously and take the user back to that channel.
Personal Touch: Add at least one additional feature to your chat application of your choosing! Feel free to be creative, but if you’re looking for ideas, possibilities include: supporting deleting one’s own messages, supporting use attachments (file uploads) as messages, or supporting private messaging between two users.
This project has 10 files that are:
- static/
- attach-file.png
- chat.js
- helper.js
- style.css
- templates/
- base_layout.html
- chat.html
- create_channel.html
- index.html
- login.html
- .flaskenv
- application.py
In this file we have the template base where I used the Bootstrap Navbar components in addition to css and javascript. In addition, the global variables below that take the values of the cookie and localStorage are declared.
var current_username = getCookie("username");
var last_channel = localStorage.getItem('last_channel');
The excerpt below is responsible for listing the groups created according to Requirement 3.
<div class="col-sm-3">
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h4>Channels</h4>
<ul class="list-group">
{%for channel in channel_list%}
<li class="list-group-item"><a
href="{{url_for('channel',channelid=channel)}}">#{{channel}}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
To satisfy Requirement 6,I created the javascript below to retrieve the last channel accessed by the user and direct it to that channel.
if (current_username!="") {
if (last_channel !="" && last_channel!=null) {
window.location.replace('/channel/' + last_channel);
}
}
This file contains the login form and the javascript below to satisfy requirements Requirement 1 and Requirement 6.
if (current_username == "") {
if (last_channel != "") {
localStorage.removeItem('last_channel');
}
}
In this file is the channel creation form as per Requirement 2.
<div class="jumbotron">
<form action="/createchannel" method="POST" class="needs-validation" novalidate>
<div class="form-group">
<label for="textChannelName">Channel Name:</label>
<input type="text" class="form-control" name="channel_name" id="textChannelName" required>
<div class="invalid-feedback">
Please provide a channel name!
</div>
</div>
<button type="submit" class="btn btn-success my-1">Create Channel</button>
</form>
</div>
This file contains the chat frontend rules. It has the div #chat_list that contains all the messages sent and received.
This file contains the javascript code that controls the entire chat screen. In the section below I connect to the socket.
var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port);
The events below are used to enter and leave a channel at the click of a button.
document.querySelector('#btn_join_channel').addEventListener("click", function (event) {
socket.emit('join a room');
$("#txMessage").prop("disabled", false);
$("#file_upload").prop("disabled", false);
localStorage.setItem('last_channel', current_channel)
this.style.visibility = "hidden";
document.querySelector('#btn_exit_channel').style.visibility = "visible";
});
document.querySelector('#btn_exit_channel').addEventListener("click", function (event) {
socket.emit('exit a room');
$("#txMessage").prop("disabled", true);
$("#file_upload").prop("disabled", true);
localStorage.removeItem('last_channel');
this.style.visibility = "hidden";
document.querySelector('#btn_join_channel').style.visibility = "visible";
});
To satisfy Requirement 7 I created the features below: This click event is used to delete messages after clicking the X button.
document.querySelectorAll("[id^='btn_delete_message_']").forEach(function (button) {
button.onclick = function () {
let id = this.dataset.id;
socket.emit('delete message', id);
}
});
The event below is used to upload the files by converting them to base64 string and sending through the socket
document.querySelector('#file_upload').onchange = function () {
var file = this.files[0],
reader = new FileReader();
reader.onloadend = function () {
var b64 = reader.result;
socket.emit('send file', b64, file.name, file.type);
};
reader.readAsDataURL(file);
};
The sections below are used to receive the return of the sockets.
socket.on('return message', data => {
sendMessenger(data);
});
socket.on('return message file', data => {
sendMessenger(data);
});
socket.on('room status', data => {
sendMessenger(data);
});
socket.on('return deleted message', data => {
if (data.id != "undefined") {
document.querySelector('#id_' + data.id).remove();
}
});
The send Messenger function receives the data object and dynamically creates all the html elements that make up the messages according to Requirement 4 and Requirement 5.
function sendMessenger(data) {
let div_class = "container darker";
let span_class = "time-left";
let h3_class = "right";
if (current_username == data.username) {
div_class = "container";
span_class = "time-right";
h3_class = "";
}
const br = document.createElement('BR');
const div = document.createElement('div');
const button = document.createElement('BUTTON');
const span2 = document.createElement('span');
const h3 = document.createElement('h3');
const a = document.createElement('A');
var img = document.createElement("IMG");
const p = document.createElement('p');
const span = document.createElement('SPAN');
span.className = span_class;
div.className = div_class;
div.setAttribute("id", "id_" + data.id);
if (current_username == data.username) {
button.className = "close delete-button";
button.setAttribute("data-id", data.id);
button.setAttribute("id", "btn_delete_message_" + data.id);
button.setAttribute("type", "submit");
button.setAttribute("aria-label", "Close");
button.onclick = function () {
let id = this.dataset.id;
socket.emit('delete message', id);
};
span2.setAttribute("aria-hidden", "true");
span2.innerHTML = "×";
button.append(span2);
div.append(button);
}
h3.innerHTML = data.username;
h3.className = h3_class;
span.className = "time-right";
span.innerHTML = data.timestamp;
div.append(h3);
a.setAttribute("href", data.msg);
a.setAttribute("target", "_blank");
a.setAttribute("download", data.filename);
if (data.type == "IMAGE") {
img.src = data.msg;
img.width = "250";
img.className = "img-fluid img-thumbnail";
a.append(img);
div.append(a);
div.append(br);
p.className = "text-center text-break";
p.innerHTML = data.filename;
div.append(p);
document.querySelector('#file_upload').value = "";
}
else if (data.type == "OTHER") {
img.src = file_icon;
img.width = "250";
img.className = "img-fluid img-thumbnail";
a.append(img);
div.append(a);
div.append(br);
p.className = "text-center text-break";
p.innerHTML = data.filename;
div.append(p);
document.querySelector('#file_upload').value = "";
}
else {
p.className = "text-left";
p.innerHTML = data.msg;
div.append(p);
}
div.append(span);
document.querySelector('#chat_list').append(div);
setTimeout(() => {
var chatList = document.getElementById('chat_list');
chatList.scrollTop = chatList.scrollHeight;
}, 100);
}
The getCookie function takes the name of a cookie and returns its result.
function getCookie(cookie_name) {
var name = cookie_name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var list_cookies = decodedCookie.split(';');
for(var i = 0; i <list_cookies.length; i++) {
var cookie = list_cookies[i];
while (cookie.charAt(0) == ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf(name) == 0) {
return cookie.substring(name.length, cookie.length);
}
}
return "";
}
In this file are all the backend rules, I will detail them later.
The is_authenticated function checks whether a user is authenticated using cookies.
def is_authenticated():
username = request.cookies.get('username')
if not username is None and username:
if not username in logged_user_list:
logged_user_list[username] = ""
return True
else:
return False
List of supported image formats:
image_files_list = ["image/bmp", "image/gif",
"image/x-icon", "image/jpeg", "image/png", "image/svg+xml"]
Below we have a dictionary containing the groups created and a dictionary for the logged-in users.
channel_list = dict()
logged_user_list = dict()
The method below performs the creation of the user and recording it in the cookies( Requirement 1 ).
@app.route("/login", methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template(LOGIN_TEMPLATE)
elif request.method == 'POST':
username = request.form.get("username")
username=username.replace("'","")
if username in logged_user_list:
flash("The username already exists!", "danger")
return render_template(LOGIN_TEMPLATE)
else:
logged_user_list[username] = ""
resp = make_response(redirect("/"))
resp.set_cookie("username", username,max_age=99999999)
return resp
The method below performs the user's logout excluding him from the cookies and from the dictionary logged_user_list.
@app.route("/logout")
def logout():
username = request.cookies.get("username")
resp = make_response(redirect("/login"))
try:
resp.delete_cookie("username")
logged_user_list.pop(username, None)
except:
pass
flash("Logout successful!", "success")
return resp
The method below performs the creation of a new channel and does not allow the creation of channels with repeated names(Requirement 2).
@app.route("/createchannel", methods=['GET', 'POST'])
def createchannel():
if not is_authenticated():
return redirect("/login")
username = request.cookies.get('username')
if request.method == 'GET':
return render_template("create_channel.html", channel_list=channel_list, logged_user_list=logged_user_list)
elif request.method == 'POST':
channel_name = request.form.get('channel_name')
if channel_name in channel_list:
flash("The channel name already exists!", "danger")
else:
channel_list[channel_name] = deque()
flash("successfully created channel!", "success")
return render_template("create_channel.html",username=username, channel_list=channel_list, logged_user_list=logged_user_list)
Method that receives the ID of a channel and displays all messages on that channel.
@app.route("/channel/<string:channelid>", methods=['GET'])
def channel(channelid):
if not is_authenticated():
return redirect("/login")
messages = ""
try:
username = request.cookies.get("username")
logged_user_list[username]=channelid
messages = channel_list[channelid]
except:
channelid = ""
flash("Channel not found!", "danger")
return render_template("chat.html", username=username, messages=messages, name=channelid, channel_list=channel_list, logged_user_list=logged_user_list)
The socket below is used to send messages to a group and send it back to the frontend.
@socketio.on("send message")
def send_message(msg):
username = request.cookies.get("username")
current_channel=logged_user_list[username]
message = Message(username, MessageType.TEXT, msg, time.strftime(
DATETIME_TEMPLATE, time.localtime(time.time())), "")
messages = channel_list[current_channel]
if len(messages) > 100:
messages.popleft()
messages.append(message)
channel_list[current_channel] = messages
emit('return message', {
'id': message.id,
'timestamp': message.timestamp,
'username': username,
'filename': message.filename,
'type': message.type.name,
'is_delete': True,
'msg': message.text}, room=current_channel)
The socket below receives a base64 string and other information, writes everything to the dictionary channel_list and then returns everything to the frontend([Requirement 7](#requirement-7)).
@socketio.on("send file")
def send(file, name, type):
username = request.cookies.get("username")
current_channel=logged_user_list[username]
type_file = MessageType.OTHER
if type in image_files_list:
type_file = MessageType.IMAGE
message = Message(username, type_file, file, time.strftime(
DATETIME_TEMPLATE, time.localtime(time.time())), name)
messages = channel_list[current_channel]
The rule below is valid if the "deck ()" has more than 100 records it removes an item from the end of the list and adds a new item at the beginning of the list.
if len(messages) > 100:
messages.popleft()
messages.append(message)
This snippet sends the message back to the frontend.
channel_list[current_channel] = messages
emit('return message file', {
'id': message.id,
'timestamp': message.timestamp,
'username': username,
'filename': message.filename,
'type': message.type.name,
'is_delete': True,
'msg': message.text}, room=current_channel)
The socket below receives a message id and removes it from the deck () and then returns to the frontend that will delete the message there(Requirement 7).
@socketio.on("delete message")
def delete_message(id):
username = request.cookies.get("username")
current_channel=logged_user_list[username]
messages = channel_list[current_channel]
for message in messages:
if message.id == id:
messages.remove(message)
break
channel_list[current_channel] = messages
emit('return deleted message', {'id': id}, room=current_channel)
the methods below are for entering and leaving a channel.
@socketio.on("join a room")
def join_a_room():
username = request.cookies.get("username")
current_channel=logged_user_list[username]
logged_user_list[username] = current_channel
join_room(current_channel)
emit('room status', {
'timestamp': time.strftime(DATETIME_TEMPLATE, time.localtime(time.time())),
'username': username,
'msg': " joined the channel "+current_channel}, room=current_channel)
@socketio.on("exit a room")
def exit_a_room():
username = request.cookies.get("username")
current_channel=logged_user_list[username]
leave_room(current_channel)
emit('room status', {
'timestamp': time.strftime(DATETIME_TEMPLATE, time.localtime(time.time())),
'username': username,
'msg': " exit the channel "+current_channel}, room=current_channel)