Added Images and Video support. Also improved the consistently of the database and the login section

This commit is contained in:
Olai Vike Bøe 2024-06-13 19:47:01 +02:00
parent fc136ab087
commit 918d6a9800
6 changed files with 577 additions and 265 deletions

5
.gitignore vendored
View file

@ -1,2 +1,5 @@
*.db
secret.key
secret.key
app.log
/cdn
/uploads

182
app.py
View file

@ -1,6 +1,6 @@
from flask import Flask, request, render_template, redirect, url_for, session, jsonify
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory
from flask_sqlalchemy import SQLAlchemy
from cryptography.fernet import Fernet
from cryptography.fernet import Fernet, InvalidToken
import os
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
@ -10,24 +10,21 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'cdn'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
db = SQLAlchemy(app)
socketio = SocketIO(app)
# Function to generate a new encryption key
def generate_key():
key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
key_file.write(key)
# Function to load the encryption key
# Load encryption key
def load_key():
return open("secret.key", "rb").read()
# Ensure the key file exists
if not os.path.exists("secret.key"):
generate_key()
key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
key_file.write(key)
# Load the key
key = load_key()
cipher = Fernet(key)
@ -42,6 +39,7 @@ class Message(db.Model):
sender = db.Column(db.String(150), nullable=False)
receiver = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text, nullable=False)
content_type = db.Column(db.String(20), nullable=False, default='text')
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
class PendingMessage(db.Model):
@ -49,6 +47,7 @@ class PendingMessage(db.Model):
sender = db.Column(db.String(150), nullable=False)
receiver = db.Column(db.String(150), nullable=False)
content = db.Column(db.Text, nullable=False)
content_type = db.Column(db.String(20), nullable=False, default='text')
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
class Friend(db.Model):
@ -83,7 +82,7 @@ def register():
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
except:
except Exception as e:
return 'Username already exists!'
return render_template('register.html')
@ -95,6 +94,7 @@ def login():
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
session['username'] = user.username
session['user_id'] = user.id
return redirect(url_for('dashboard'))
return 'Invalid credentials'
return render_template('login.html')
@ -111,59 +111,114 @@ def dashboard():
pending_messages = PendingMessage.query.filter_by(receiver=session['username']).all()
messages = Message.query.filter_by(receiver=session['username']).all()
decrypted_pending_messages = [(msg.sender, cipher.decrypt(msg.content.encode()).decode(), msg.timestamp) for msg in pending_messages]
decrypted_messages = [(msg.sender, cipher.decrypt(msg.content.encode()).decode(), msg.timestamp) for msg in messages]
decrypted_pending_messages = []
for msg in pending_messages:
try:
decrypted_pending_messages.append((msg.sender, cipher.decrypt(msg.content.encode()).decode(), msg.timestamp))
except InvalidToken:
decrypted_pending_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp))
return render_template('dashboard.html', username=session['username'], friends=friend_users, friend_requests=friend_requests, pending_messages=decrypted_pending_messages, messages=decrypted_messages)
decrypted_messages = []
for msg in messages:
try:
decrypted_messages.append((msg.sender, cipher.decrypt(msg.content.encode()).decode(), msg.timestamp))
except InvalidToken:
decrypted_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp))
return render_template('dashboard.html', username=session['username'], user_id=user.id, friends=friend_users, friend_requests=friend_requests, pending_messages=decrypted_pending_messages, messages=decrypted_messages)
return redirect(url_for('login'))
@app.route('/chat/<int:friend_id>')
def chat(friend_id):
if 'username' in session:
current_user_id = session['user_id']
friend = Friend.query.filter_by(user_id=current_user_id, friend_id=friend_id).first()
if friend:
friend_user = User.query.filter_by(id=friend_id).first()
user = User.query.filter_by(id=current_user_id).first()
friends = Friend.query.filter_by(user_id=user.id).all()
friend_ids = [f.friend_id for f in friends]
friend_users = User.query.filter(User.id.in_(friend_ids)).all()
friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all()
return render_template('chat.html', username=session['username'], friend_id=friend_id, friend_username=friend_user.username, friends=friend_users, friend_requests=friend_requests)
else:
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
@app.route('/send_message/<receiver>', methods=['POST'])
def send_message(receiver):
if 'username' in session:
data = request.get_json()
content = data['content']
encrypted_content = cipher.encrypt(content.encode()).decode()
try:
content = request.form.get('content')
timestamp = request.form.get('timestamp')
file = request.files.get('image') or request.files.get('video')
content_type = 'text'
# Check if they are friends
user = User.query.filter_by(username=session['username']).first()
receiver_user = User.query.filter_by(username=receiver).first()
if not receiver_user:
return jsonify({'error': 'User not found'}), 404
friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first()
if file:
filename = f"{datetime.utcnow().timestamp()}_{file.filename}"
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
content = filename
content_type = 'file'
if friend:
new_message = Message(sender=session['username'], receiver=receiver, content=encrypted_content)
db.session.add(new_message)
db.session.commit()
decrypted_content = cipher.decrypt(encrypted_content.encode()).decode()
socketio.emit('new_message', {
'sender': session['username'],
'content': decrypted_content,
'timestamp': new_message.timestamp.strftime("%Y-%m-%d %H:%M:%S")
}, room=receiver)
else:
pending_message = PendingMessage(sender=session['username'], receiver=receiver, content=encrypted_content)
db.session.add(pending_message)
db.session.commit()
if content or file:
# Encrypt text content only, do not encrypt file names
encrypted_content = cipher.encrypt(content.encode()).decode() if content_type == 'text' else content
timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S')
return jsonify({'status': 'Message sent'}), 200
# Check if they are friends
user = User.query.filter_by(username=session['username']).first()
receiver_user = User.query.filter_by(id=receiver).first()
if not receiver_user:
return jsonify({'error': 'User not found'}), 404
friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first()
if friend:
new_message = Message(sender=session['username'], receiver=receiver_user.username, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt)
db.session.add(new_message)
db.session.commit()
decrypted_content = cipher.decrypt(encrypted_content.encode()).decode() if content_type == 'text' else encrypted_content
socketio.emit('new_message', {
'sender': session['username'],
'content': decrypted_content,
'content_type': content_type,
'timestamp': timestamp
}, room=receiver_user.username)
else:
pending_message = PendingMessage(sender=session['username'], receiver=receiver_user.username, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt)
db.session.add(pending_message)
db.session.commit()
return jsonify({'status': 'Message sent'}), 200
return jsonify({'error': 'No content or file provided'}), 400
except Exception as e:
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'Unauthorized'}), 401
@app.route('/get_messages/<friend_username>', methods=['GET'])
def get_messages(friend_username):
@app.route('/get_messages/<int:friend_id>', methods=['GET'])
def get_messages(friend_id):
if 'username' in session:
current_user = session['username']
messages = Message.query.filter(
((Message.sender == current_user) & (Message.receiver == friend_username)) |
((Message.sender == friend_username) & (Message.receiver == current_user))
).all()
decrypted_messages = [
{'sender': msg.sender, 'content': cipher.decrypt(msg.content.encode()).decode(), 'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
for msg in messages
]
return jsonify({'messages': decrypted_messages})
current_user_id = session['user_id']
current_user = User.query.filter_by(id=current_user_id).first()
friend_user = User.query.filter_by(id=friend_id).first()
if friend_user:
friend = Friend.query.filter_by(user_id=current_user_id, friend_id=friend_id).first()
if friend:
messages = Message.query.filter(
((Message.sender == current_user.username) & (Message.receiver == friend_user.username)) |
((Message.sender == friend_user.username) & (Message.receiver == current_user.username))
).all()
decrypted_messages = [
{'sender': msg.sender, 'content': cipher.decrypt(msg.content.encode()).decode() if msg.content_type == 'text' else msg.content, 'content_type': msg.content_type, 'timestamp': msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
for msg in messages
]
return jsonify({'messages': decrypted_messages})
return jsonify({'error': 'Unauthorized'}), 401
@app.route('/cdn/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
@app.route('/add_friend', methods=['POST'])
def add_friend():
if 'username' in session:
@ -202,7 +257,7 @@ def accept_friend(request_id):
# Move pending messages to messages
pending_messages = PendingMessage.query.filter_by(sender=friend_request.sender.username, receiver=friend_request.receiver.username).all()
for pm in pending_messages:
new_message = Message(sender=pm.sender, receiver=pm.receiver, content=pm.content, timestamp=pm.timestamp)
new_message = Message(sender=pm.sender, receiver=pm.receiver, content=pm.content, content_type=pm.content_type, timestamp=pm.timestamp)
db.session.add(new_message)
db.session.delete(pm)
db.session.commit()
@ -257,18 +312,9 @@ def remove_friend(friend_id):
@app.route('/logout')
def logout():
session.pop('username', None)
session.pop('user_id', None)
return redirect(url_for('login'))
@socketio.on('join')
def handle_join(data):
username = data['username']
join_room(username)
@socketio.on('leave')
def handle_leave(data):
username = data['username']
leave_room(username)
@socketio.on('connect')
def handle_connect():
if 'username' in session:
@ -287,7 +333,17 @@ def handle_disconnect():
db.session.commit()
emit('user_offline', {'username': user.username}, broadcast=True)
@socketio.on('join')
def handle_join(data):
username = data['username']
join_room(username)
@socketio.on('leave')
def handle_leave(data):
username = data['username']
leave_room(username)
if __name__ == '__main__':
with app.app_context():
db.create_all()
socketio.run(app, debug=True)
socketio.run(app, host='0.0.0.0', port=8086, debug=True)

View file

@ -1,8 +1,11 @@
/* styles.css */
body {
display: flex;
height: 100vh;
margin: 0;
}
.sidebar {
width: 250px;
background-color: #2f3136;
@ -10,25 +13,151 @@ body {
padding: 20px;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.sidebar h3 {
margin-top: 0;
}
.sidebar ul {
list-style: none;
padding: 0;
}
.sidebar ul li {
margin: 10px 0;
cursor: pointer;
display: flex;
align-items: center;
}
.sidebar ul li .status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
}
.content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.friend-request {
margin-bottom: 20px;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
.chat-header {
background-color: #36393f;
color: white;
padding: 10px;
margin-bottom: 20px;
}
.messages {
flex: 1;
overflow-y: auto;
margin-bottom: 20px;
}
.message {
margin-bottom: 10px;
position: relative;
}
.message strong {
color: #7289da;
}
.message .timestamp {
display: none;
position: absolute;
top: -20px;
right: 0;
background-color: rgba(0,0,0,0.7);
color: white;
padding: 5px;
border-radius: 3px;
}
.message:hover .timestamp {
display: block;
}
.send-message-form {
display: flex;
align-items: center;
}
.send-message-form input[type="text"] {
flex: 1;
padding: 10px;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.send-message-form button {
padding: 10px;
border: none;
background-color: #7289da;
color: white;
border-radius: 5px;
cursor: pointer;
}
.image-preview {
position: relative;
display: inline-block;
}
.image-preview img {
max-width: 100px;
max-height: 100px;
display: block;
}
.image-preview .remove-image {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
}
/* Image enhancer */
.image-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
justify-content: center;
align-items: center;
z-index: 1000;
}
.image-overlay img {
max-width: 90%;
max-height: 90%;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
border-radius: 10px;
cursor: pointer;
}
.online{
margin-right: 10px;
height: 10px;
@ -42,60 +171,4 @@ body {
width: 10px;
background-color: #80848e;
border-radius: 50%;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow-y: auto;
}
.chat-header {
background-color: #36393f;
color: white;
padding: 10px;
margin-bottom: 20px;
}
.messages {
flex: 1;
overflow-y: auto;
margin-bottom: 20px;
}
.message {
margin-bottom: 10px;
position: relative;
}
.message strong {
color: #7289da;
}
.message .timestamp {
display: none;
position: absolute;
top: -20px;
right: 0;
background-color: rgba(0,0,0,0.7);
color: white;
padding: 5px;
border-radius: 3px;
}
.message:hover .timestamp {
display: block;
}
.send-message-form {
display: flex;
}
.send-message-form input {
flex: 1;
padding: 10px;
margin-right: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.send-message-form button {
padding: 10px;
border: none;
background-color: #7289da;
color: white;
border-radius: 5px;
cursor: pointer;
}

237
templates/chat.html Normal file
View file

@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
socket.emit('join', { username: "{{ username }}" });
const messagesList = document.getElementById('messages');
const imageOverlay = document.getElementById('image-overlay');
const overlayImage = document.getElementById('overlay-image');
function scrollToBottom() {
messagesList.scrollTop = messagesList.scrollHeight;
}
socket.on('new_message', function(data) {
const newMessage = document.createElement('div');
newMessage.classList.add('message');
if (data.content_type === 'text') {
newMessage.innerHTML = `<strong>${data.sender}</strong>: ${data.content}<div class="timestamp">${data.timestamp}</div>`;
} else if (data.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(data.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(data.content);
if (isImage) {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <img src="/cdn/${data.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${data.timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${data.content}" type="video/mp4"></video><div class="timestamp">${data.timestamp}</div>`;
} else {
newMessage.innerHTML = `<strong>${data.sender}</strong>: <a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${data.timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
});
const friendId = "{{ friend_id }}"; // Ensure this is treated as a string
if (friendId) {
fetch(`/get_messages/${friendId}`)
.then(response => response.json())
.then(data => {
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Chatting with {{ friend_username }}`;
document.getElementById('send-message-form').dataset.receiver = friendId;
data.messages.forEach(msg => {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
if (msg.content_type === 'text') {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: ${msg.content}<div class="timestamp">${msg.timestamp}</div>`;
} else if (msg.content_type === 'file') {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(msg.content);
const isVideo = /\.(mp4|webm|ogg)$/i.test(msg.content);
if (isImage) {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <img src="/cdn/${msg.content}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${msg.timestamp}</div>`;
} else if (isVideo) {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="/cdn/${msg.content}" type="video/mp4"></video><div class="timestamp">${msg.timestamp}</div>`;
} else {
messageElement.innerHTML = `<strong>${msg.sender}</strong>: <a href="/cdn/${msg.content}" target="_blank">Download File</a><div class="timestamp">${msg.timestamp}</div>`;
}
}
messagesList.appendChild(messageElement);
});
scrollToBottom();
})
.catch(error => console.error('Error fetching messages:', error));
}
const sendMessageForm = document.querySelector('.send-message-form');
if (sendMessageForm) {
sendMessageForm.addEventListener('submit', function(event) {
event.preventDefault();
const receiver = this.dataset.receiver;
const contentInput = this.querySelector('input[name="content"]');
const fileInput = this.querySelector('input[name="image"]') || this.querySelector('input[name="video"]');
const content = contentInput.value;
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
const imagePreview = document.querySelector('.image-preview img');
const videoPreview = document.querySelector('.video-preview video');
if (content || fileInput.files.length > 0) {
// Append the message locally
const newMessage = document.createElement('div');
newMessage.classList.add('message');
if (content) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: ${content}<div class="timestamp">${timestamp}</div>`;
}
if (fileInput.files.length > 0) {
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(fileInput.files[0].name);
const isVideo = /\.(mp4|webm|ogg)$/i.test(fileInput.files[0].name);
if (isImage) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <img src="${imagePreview.src}" alt="Image" class="enhanceable-image" style="max-width: 200px; max-height: 200px;" /><div class="timestamp">${timestamp}</div>`;
} else if (isVideo) {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <video controls style="max-width: 200px; max-height: 200px;"><source src="${videoPreview.src}" type="video/mp4"></video><div class="timestamp">${timestamp}</div>`;
} else {
newMessage.innerHTML = `<strong>{{ username }}</strong>: <a href="${fileInput.src}" target="_blank">Download File</a><div class="timestamp">${timestamp}</div>`;
}
}
messagesList.appendChild(newMessage);
scrollToBottom();
const formData = new FormData();
formData.append('content', content);
formData.append('timestamp', timestamp);
if (fileInput.files[0]) {
formData.append('image', fileInput.files[0]);
}
fetch(`/send_message/${receiver}`, {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Message sent') {
contentInput.value = '';
fileInput.value = '';
document.querySelector('.image-preview').style.display = 'none';
document.querySelector('.image-preview img').src = '';
document.querySelector('.video-preview').style.display = 'none';
document.querySelector('.video-preview video').src = '';
} else {
alert('Message sending failed');
}
});
}
});
document.querySelector('input[name="image"]').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const imagePreview = document.querySelector('.image-preview');
imagePreview.style.display = 'block';
imagePreview.querySelector('img').src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.querySelector('input[name="video"]').addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const videoPreview = document.querySelector('.video-preview');
videoPreview.style.display = 'block';
videoPreview.querySelector('video').src = e.target.result;
};
reader.readAsDataURL(file);
}
});
document.querySelector('.remove-image').addEventListener('click', function() {
const fileInput = document.querySelector('input[name="image"]');
fileInput.value = '';
const imagePreview = document.querySelector('.image-preview');
imagePreview.style.display = 'none';
imagePreview.querySelector('img').src = '';
});
document.querySelector('.remove-video').addEventListener('click', function() {
const fileInput = document.querySelector('input[name="video"]');
fileInput.value = '';
const videoPreview = document.querySelector('.video-preview');
videoPreview.style.display = 'none';
videoPreview.querySelector('video').src = '';
});
}
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
// Image enhancer functionality
document.addEventListener('click', function(event) {
if (event.target.classList.contains('enhanceable-image')) {
overlayImage.src = event.target.src;
imageOverlay.style.display = 'flex';
}
});
imageOverlay.addEventListener('click', function() {
imageOverlay.style.display = 'none';
});
});
</script>
</head>
<body>
{% include 'sidebar.html' %}
<div class="chat-container">
<div class="chat-header">
<h3 id="chat-with">Chatting with {{ friend_username }}</h3>
</div>
<div class="messages" id="messages">
<!-- Messages will be displayed here -->
</div>
<form class="send-message-form" id="send-message-form" data-receiver="{{ friend_id }}" method="POST">
<input type="text" name="content" placeholder="Type a message">
<input type="file" name="image" style="display: none;">
<input type="file" name="video" style="display: none;">
<button type="button" onclick="document.querySelector('input[name=\'image\']').click()">+</button>
<button type="button" onclick="document.querySelector('input[name=\'video\']').click()">+</button>
<div class="image-preview" style="display: none;">
<img src="" alt="Image Preview">
<button type="button" class="remove-image">X</button>
</div>
<div class="video-preview" style="display: none;">
<video controls>
<source src="" type="video/mp4">
</video>
<button type="button" class="remove-video">X</button>
</div>
<button type="submit">Send</button>
</form>
<div id="image-overlay" class="image-overlay">
<img id="overlay-image" src="" alt="Enhanced Image">
</div>
</div>
</body>
</html>

View file

@ -5,182 +5,109 @@
<title>Dashboard</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
<script type="text/javascript">
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const socket = io();
socket.emit('join', { username: "{{ username }}" });
socket.on('new_message', function(data) {
const messagesList = document.getElementById('messages');
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.innerHTML = `<strong>${data.sender}</strong>: ${data.content}<div class="timestamp">${data.timestamp}</div>`;
messagesList.appendChild(newMessage);
});
socket.on('friend_request', function(data) {
const friendRequestsList = document.getElementById('friend-requests');
const newRequest = document.createElement('li');
newRequest.innerHTML = `
${data.sender}
<form method="POST" action="/accept_friend/${data.request_id}">
<button type="submit">Accept</button>
</form>
<form method="POST" action="/reject_friend/${data.request_id}">
<button type="submit">Reject</button>
</form>
`;
friendRequestsList.appendChild(newRequest);
checkFriendRequests();
});
socket.on('friend_request_accepted', function(data) {
const friendList = document.getElementById('friends');
const newFriend = document.createElement('li');
newFriend.innerHTML = `
<div class="status-indicator ${data.online ? 'online' : 'offline'}"></div>
${data.sender}
<form class="send-message-form" data-receiver="${data.sender}" method="POST">
<input type="text" name="content" placeholder="Type a message" required>
<button type="submit">Send</button>
</form>
`;
friendList.appendChild(newFriend);
checkFriendRequests();
});
socket.on('user_online', function(data) {
const friendElements = document.querySelectorAll(`.friend[data-username="${data.username}"] .status-indicator`);
friendElements.forEach(el => el.classList.remove('offline'));
friendElements.forEach(el => el.classList.add('online'));
});
socket.on('user_offline', function(data) {
const friendElements = document.querySelectorAll(`.friend[data-username="${data.username}"] .status-indicator`);
friendElements.forEach(el => el.classList.remove('online'));
friendElements.forEach(el => el.classList.add('offline'));
});
const friends = document.querySelectorAll('.friend');
friends.forEach(friend => {
friend.addEventListener('click', function() {
const friendName = this.dataset.username;
loadMessages(friendName);
});
});
function loadMessages(friendName) {
fetch(`/get_messages/${friendName}`)
.then(response => response.json())
.then(data => {
const messagesList = document.getElementById('messages');
messagesList.innerHTML = '';
document.getElementById('chat-with').textContent = `Chatting with ${friendName}`;
document.getElementById('send-message-form').dataset.receiver = friendName;
data.messages.forEach(msg => {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.innerHTML = `<strong>${msg.sender}</strong>: ${msg.content}<div class="timestamp">${msg.timestamp}</div>`;
messagesList.appendChild(messageElement);
});
});
}
function checkFriendRequests() {
const friendRequestsList = document.getElementById('friend-requests');
const friendRequestsSection = document.getElementById('friend-requests-section');
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
if (friendRequestsList && friendRequestsSection) {
if (friendRequestsList.children.length === 0) {
friendRequestsSection.style.display = 'none';
} else {
friendRequestsSection.style.display = 'block';
}
}
}
checkFriendRequests();
document.querySelectorAll('.send-message-form').forEach(form => {
form.addEventListener('submit', function(event) {
// Add event listeners if elements exist
const acceptButtons = document.querySelectorAll('.accept-friend-request');
const rejectButtons = document.querySelectorAll('.reject-friend-request');
acceptButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/accept_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request accepted') {
// Handle UI updates if necessary
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
rejectButtons.forEach(button => {
button.addEventListener('click', function() {
const requestId = this.dataset.requestId;
fetch(`/reject_friend/${requestId}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.status === 'Friend request rejected') {
// Handle UI updates if necessary
this.closest('.friend-request').remove();
checkFriendRequests();
}
});
});
});
const addFriendForm = document.getElementById('add-friend-form');
if (addFriendForm) {
addFriendForm.addEventListener('submit', function(event) {
event.preventDefault();
const receiver = form.dataset.receiver;
const content = form.querySelector('input[name="content"]').value;
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
// Append the message locally
const messagesList = document.getElementById('messages');
const newMessage = document.createElement('div');
newMessage.classList.add('message');
newMessage.innerHTML = `<strong>{{ username }}</strong>: ${content}<div class="timestamp">${timestamp}</div>`;
messagesList.appendChild(newMessage);
fetch(`/send_message/${receiver}`, {
const formData = new FormData(addFriendForm);
fetch('/add_friend', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ content: content })
body: formData
}).then(response => response.json())
.then(data => {
if (data.status === 'Message sent') {
form.querySelector('input[name="content"]').value = '';
if (data.status === 'Friend request sent') {
alert('Friend request sent');
} else {
alert('Message sending failed');
alert('Failed to send friend request');
}
});
});
});
}
});
</script>
</head>
<body>
<div class="sidebar">
<h3>Welcome, {{ username }}</h3>
<form method="POST" action="{{ url_for('add_friend') }}">
<label for="friend_username">Add Friend:</label>
<input type="text" id="friend_username" name="friend_username" required>
<button type="submit">Add Friend</button>
</form>
<a href="{{ url_for('logout') }}" class="btn btn-danger mt-3">Logout</a>
{% include 'sidebar.html' %}
<div class="content">
<h4>Friends</h4>
<ul id="friends-list">
{% for friend in friends %}
<li>
<a href="/chat/{{ friend.id }}">{{ friend.username }}</a>
</li>
{% endfor %}
</ul>
<div id="friend-requests-section">
<h4>Friend Requests</h4>
<ul id="friend-requests">
{% for request in friend_requests %}
<li>
{{ request.sender.username }}
<form method="POST" action="{{ url_for('accept_friend', request_id=request.id) }}">
<button type="submit">Accept</button>
</form>
<form method="POST" action="{{ url_for('reject_friend', request_id=request.id) }}">
<button type="submit">Reject</button>
</form>
</li>
<li class="friend-request">
{{ request.sender.username }}
<button class="accept-friend-request" data-request-id="{{ request.id }}">Accept</button>
<button class="reject-friend-request" data-request-id="{{ request.id }}">Reject</button>
</li>
{% endfor %}
</ul>
</div>
<h4>Friends</h4>
<ul id="friends">
{% for friend in friends %}
<li class="friend" data-username="{{ friend.username }}">
<div class="status-indicator {{ 'online' if friend.online else 'offline' }}"></div>
{{ friend.username }}
</li>
{% endfor %}
</ul>
</div>
<div class="chat-container">
<div class="chat-header">
<h3 id="chat-with">Select a friend to start chatting</h3>
<div>
<h4>Add Friend</h4>
<form id="add-friend-form" method="POST">
<input type="text" name="friend_username" placeholder="Friend's Username" required>
<button type="submit">Add Friend</button>
</form>
</div>
<div class="messages" id="messages">
<!-- Messages will be displayed here -->
</div>
<form class="send-message-form" id="send-message-form" data-receiver="" method="POST">
<input type="text" name="content" placeholder="Type a message" required>
<button type="submit">Send</button>
</form>
</div>
</body>
</html>

16
templates/sidebar.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sidebar</title>
</head>
<body>
<div class="sidebar">
<h3>Welcome, {{ username }}</h3>
<ul>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</body>
</html>