From b5937c03e6857fc3c924de873aa81ef8160da961 Mon Sep 17 00:00:00 2001 From: Olai Date: Fri, 14 Jun 2024 11:35:53 +0200 Subject: [PATCH] Added new login screen and MD --- app.py | 98 ++++++++++-- requirements.txt | 1 + static/js/chat.js | 332 +++++++++++++++++++++++++++++++++++++++ static/js/dashboard.js | 85 ++++++++++ static/styles.css | 128 +++++++++++++-- templates/chat.html | 238 ++++------------------------ templates/dashboard.html | 81 +--------- templates/login.html | 46 ++++-- templates/sidebar.html | 15 +- 9 files changed, 697 insertions(+), 327 deletions(-) create mode 100644 static/js/chat.js create mode 100644 static/js/dashboard.js diff --git a/app.py b/app.py index 5eb0f19..f381c23 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,17 @@ -from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory +from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory, make_response from flask_sqlalchemy import SQLAlchemy from cryptography.fernet import Fernet, InvalidToken import os -from datetime import datetime +from datetime import datetime, timedelta, timezone from werkzeug.security import generate_password_hash, check_password_hash from flask_socketio import SocketIO, emit, join_room, leave_room +import logging +import jwt + +# Set up logging +logging.basicConfig(filename='app.log', level=logging.DEBUG, + format='%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]', filemode='w') +logger = logging.getLogger(__name__) app = Flask(__name__) app.config['SECRET_KEY'] = os.urandom(24) @@ -28,6 +35,24 @@ if not os.path.exists("secret.key"): key = load_key() cipher = Fernet(key) +# JWT Utility Functions +def generate_token(user_id): + payload = { + 'user_id': user_id, + 'exp': datetime.now(timezone.utc) + timedelta(days=7) + } + token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') + return token + +def decode_token(token): + try: + payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) + return payload['user_id'] + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) @@ -40,7 +65,7 @@ class Message(db.Model): 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) + timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc)) class PendingMessage(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -48,7 +73,7 @@ class PendingMessage(db.Model): 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) + timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc)) class Friend(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -67,8 +92,15 @@ class FriendRequest(db.Model): @app.route('/') def index(): - if 'username' in session: - return redirect(url_for('dashboard')) + token = request.cookies.get('token') + if token: + user_id = decode_token(token) + if user_id: + user = User.query.get(user_id) + if user: + session['username'] = user.username + session['user_id'] = user.id + return redirect(url_for('dashboard')) return redirect(url_for('login')) @app.route('/register', methods=['GET', 'POST']) @@ -81,8 +113,10 @@ def register(): try: db.session.add(new_user) db.session.commit() + logger.info(f"New user registered: {username}") return redirect(url_for('login')) except Exception as e: + logger.error(f"Error registering user {username}: {e}") return 'Username already exists!' return render_template('register.html') @@ -95,8 +129,14 @@ def login(): 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' + token = generate_token(user.id) + response = make_response(jsonify({'status': 'success'})) + expires = datetime.now(timezone.utc) + timedelta(days=7) + response.set_cookie('token', token, httponly=True, expires=expires) + logger.info(f"User logged in: {username}") + return response + logger.warning(f"Invalid login attempt for user: {username}") + return jsonify({'status': 'error'}) return render_template('login.html') @app.route('/dashboard') @@ -125,6 +165,7 @@ def dashboard(): except InvalidToken: decrypted_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp)) + logger.info(f"User {session['username']} accessed dashboard") 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')) @@ -141,8 +182,10 @@ def chat(friend_id): friend_users = User.query.filter(User.id.in_(friend_ids)).all() friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').all() + logger.info(f"User {session['username']} opened chat with {friend_user.username}") 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: + logger.warning(f"User {session['username']} attempted to access chat with non-friend user_id: {friend_id}") return redirect(url_for('dashboard')) return redirect(url_for('login')) @@ -152,24 +195,25 @@ def send_message(receiver): try: content = request.form.get('content') timestamp = request.form.get('timestamp') - file = request.files.get('image') or request.files.get('video') + file = request.files.get('file') content_type = 'text' if file: - filename = f"{datetime.utcnow().timestamp()}_{file.filename}" + filename = f"{datetime.now(timezone.utc).timestamp()}_{file.filename}" file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) content = filename content_type = 'file' - if content or file: + if content: # 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') + timestamp_dt = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc) # 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: + logger.error(f"Message send failed: User not found {receiver}") return jsonify({'error': 'User not found'}), 404 friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first() @@ -182,16 +226,21 @@ def send_message(receiver): 'sender': session['username'], 'content': decrypted_content, 'content_type': content_type, - 'timestamp': timestamp + 'timestamp': timestamp, + 'id': new_message.id }, room=receiver_user.username) + logger.info(f"Message sent from {session['username']} to {receiver_user.username}") + return jsonify({'status': 'Message sent', 'message_id': new_message.id}), 200 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() + logger.info(f"Pending message from {session['username']} to {receiver_user.username}") - return jsonify({'status': 'Message sent'}), 200 + return jsonify({'status': 'Pending message sent', 'message_id': pending_message.id}), 200 return jsonify({'error': 'No content or file provided'}), 400 except Exception as e: + logger.error(f"Error sending message from {session['username']} to {receiver}: {e}") return jsonify({'error': str(e)}), 500 return jsonify({'error': 'Unauthorized'}), 401 @@ -212,7 +261,9 @@ def get_messages(friend_id): {'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 ] + logger.info(f"Messages retrieved for chat between {current_user.username} and {friend_user.username}") return jsonify({'messages': decrypted_messages}) + logger.warning(f"Unauthorized message retrieval attempt by user {session.get('username', 'unknown')}") return jsonify({'error': 'Unauthorized'}), 401 @app.route('/cdn/') @@ -233,7 +284,9 @@ def add_friend(): 'sender': session['username'], 'receiver': friend_username }, room=friend_username) + logger.info(f"Friend request sent from {session['username']} to {friend_username}") return redirect(url_for('dashboard')) + logger.warning(f"Friend request failed from {session['username']} to {friend_username}: Friend not found or cannot add yourself as a friend") return 'Friend not found or cannot add yourself as a friend' return redirect(url_for('login')) @@ -266,8 +319,10 @@ def accept_friend(request_id): 'sender': friend_request.sender.username, 'receiver': friend_request.receiver.username }, room=friend_request.sender.username) + logger.info(f"Friend request accepted by {session['username']} from {friend_request.sender.username}") return redirect(url_for('dashboard')) + logger.warning(f"Friend request accept failed: Friend request not found or unauthorized access by {session['username']}") return 'Friend request not found' return redirect(url_for('login')) @@ -283,8 +338,10 @@ def reject_friend(request_id): 'sender': friend_request.sender.username, 'receiver': friend_request.receiver.username }, room=friend_request.sender.username) + logger.info(f"Friend request rejected by {session['username']} from {friend_request.sender.username}") return redirect(url_for('dashboard')) + logger.warning(f"Friend request reject failed: Friend request not found or unauthorized access by {session['username']}") return 'Friend request not found' return redirect(url_for('login')) @@ -304,8 +361,10 @@ def remove_friend(friend_id): 'sender': session['username'], 'receiver': friend.friend.username }, room=friend.friend.username) + logger.info(f"Friend {friend.friend.username} removed by {session['username']}") return redirect(url_for('dashboard')) + logger.warning(f"Friend removal failed: Friend not found or unauthorized access by {session['username']}") return 'Friend not found' return redirect(url_for('login')) @@ -313,7 +372,10 @@ def remove_friend(friend_id): def logout(): session.pop('username', None) session.pop('user_id', None) - return redirect(url_for('login')) + response = make_response(redirect(url_for('login'))) + response.set_cookie('token', '', expires=0) + logger.info(f"User logged out") + return response @socketio.on('connect') def handle_connect(): @@ -323,6 +385,7 @@ def handle_connect(): user.online = True db.session.commit() emit('user_online', {'username': user.username}, broadcast=True) + logger.info(f"User {session['username']} connected") @socketio.on('disconnect') def handle_disconnect(): @@ -332,18 +395,21 @@ def handle_disconnect(): user.online = False db.session.commit() emit('user_offline', {'username': user.username}, broadcast=True) + logger.info(f"User {session['username']} disconnected") @socketio.on('join') def handle_join(data): username = data['username'] join_room(username) + logger.info(f"User {username} joined room {username}") @socketio.on('leave') def handle_leave(data): username = data['username'] leave_room(username) + logger.info(f"User {username} left room {username}") if __name__ == '__main__': with app.app_context(): db.create_all() - socketio.run(app, host='0.0.0.0', port=8086, debug=True) + socketio.run(app, debug=True) # host='0.0.0.0', port=8086, diff --git a/requirements.txt b/requirements.txt index 3f1fab4..47f4622 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ cryptography==42.0.8 Werkzeug==3.0.3 python-socketio==5.11.2 python-engineio==4.9.1 +PyJWT==2.8.0 \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..9ebdf16 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,332 @@ +document.addEventListener('DOMContentLoaded', (event) => { + const socket = io(); + const md = window.markdownit(); + socket.emit('join', { username: username }); + + document.title = `Chatting with ${friendUsername}`; + + const messagesList = document.getElementById('messages'); + const imageOverlay = document.getElementById('image-overlay'); + const overlayImage = document.getElementById('overlay-image'); + const contextMenu = document.getElementById('context-menu'); + const toast = document.getElementById('toast'); + const toastMessage = document.getElementById('toast-message'); + let currentMessageId = null; + let currentMessageText = null; + + function showToast(message) { + toastMessage.textContent = message; + toast.classList.add('show'); + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); + } + + function scrollToBottom() { + messagesList.scrollTop = messagesList.scrollHeight; + } + + function shouldShowUsername(previousTimestamp, currentTimestamp) { + const tenMinutes = 10 * 60 * 1000; + return (currentTimestamp - previousTimestamp) > tenMinutes; + } + + let previousSender = null; + let previousTimestamp = 0; + + socket.on('new_message', function(data) { + const currentTimestamp = new Date(data.timestamp).getTime(); + const showUsername = previousSender !== data.sender || shouldShowUsername(previousTimestamp, currentTimestamp); + previousSender = data.sender; + previousTimestamp = currentTimestamp; + + const newMessage = document.createElement('div'); + newMessage.classList.add('message'); + newMessage.dataset.messageId = data.id; // Ensure this is correct + newMessage.dataset.messageText = data.content; + + if (showUsername) { + const usernameElement = document.createElement('div'); + usernameElement.classList.add('message-sender'); + usernameElement.innerHTML = `${data.sender}:`; + messagesList.appendChild(usernameElement); + } + + if (data.content_type === 'text') { + newMessage.innerHTML = `${md.render(data.content)}
${data.timestamp}
`; + } 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 = `Image
${data.timestamp}
`; + } else if (isVideo) { + newMessage.innerHTML = `
${data.timestamp}
`; + } else { + newMessage.innerHTML = `Download File
${data.timestamp}
`; + } + } + + messagesList.appendChild(newMessage); + scrollToBottom(); + }); + + if (friendId) { + fetch(`/get_messages/${friendId}`) + .then(response => response.json()) + .then(data => { + messagesList.innerHTML = ''; + document.getElementById('chat-with').textContent = `Chatting with ${friendUsername}`; + document.getElementById('send-message-form').dataset.receiver = friendId; + + data.messages.forEach((msg, index, messages) => { + const currentTimestamp = new Date(msg.timestamp).getTime(); + const previousMessage = messages[index - 1]; + const previousTimestamp = previousMessage ? new Date(previousMessage.timestamp).getTime() : 0; + const showUsername = index === 0 || msg.sender !== previousMessage.sender || shouldShowUsername(previousTimestamp, currentTimestamp); + + if (showUsername) { + const usernameElement = document.createElement('div'); + usernameElement.classList.add('message-sender'); + usernameElement.innerHTML = `${msg.sender}:`; + messagesList.appendChild(usernameElement); + } + + const messageElement = document.createElement('div'); + messageElement.classList.add('message'); + messageElement.dataset.messageId = msg.id; + messageElement.dataset.messageText = msg.content; + + if (msg.content_type === 'text') { + messageElement.innerHTML = `${md.render(msg.content)}
${msg.timestamp}
`; + } 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 = `Image
${msg.timestamp}
`; + } else if (isVideo) { + messageElement.innerHTML = `
${msg.timestamp}
`; + } else { + messageElement.innerHTML = `Download File
${msg.timestamp}
`; + } + } + + 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="file"]'); + const content = contentInput.value; + const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' '); + const imagePreview = document.querySelector('.file-preview-image'); + const videoPreview = document.querySelector('.file-preview-video'); + const filenamePreview = document.querySelector('.file-preview-filename'); + + if (content || fileInput.files.length > 0) { + // Append the message locally + const newMessage = document.createElement('div'); + newMessage.classList.add('message'); + newMessage.dataset.messageId = 'temp-id'; // Temporary ID for the new message + const currentTimestamp = new Date(timestamp).getTime(); + const showUsername = previousSender !== username || shouldShowUsername(previousTimestamp, currentTimestamp); + previousSender = username; + previousTimestamp = currentTimestamp; + + if (showUsername) { + const usernameElement = document.createElement('div'); + usernameElement.classList.add('message-sender'); + usernameElement.innerHTML = `${username}:`; + messagesList.appendChild(usernameElement); + } + + if (content) { + newMessage.dataset.messageText = content; + newMessage.innerHTML = `${md.render(content)}
${timestamp}
`; + } + if (fileInput.files.length > 0) { + const file = fileInput.files[0]; + const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(file.name); + const isVideo = /\.(mp4|webm|ogg)$/i.test(file.name); + if (isImage) { + newMessage.innerHTML = `Image
${timestamp}
`; + } else if (isVideo) { + newMessage.innerHTML = `
${timestamp}
`; + } else { + newMessage.innerHTML = `Download File
${timestamp}
`; + } + } + messagesList.appendChild(newMessage); + scrollToBottom(); + + const formData = new FormData(); + formData.append('content', content); + formData.append('timestamp', timestamp); + if (fileInput.files[0]) { + formData.append('file', 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('.file-preview').style.display = 'none'; + document.querySelector('.file-preview img').src = ''; + document.querySelector('.file-preview video').style.display = 'none'; + document.querySelector('.file-preview video').src = ''; + document.querySelector('.file-preview .file-preview-filename').style.display = 'none'; + document.querySelector('.file-preview .file-preview-filename').textContent = ''; + + // Update the message ID with the real one from the server + newMessage.dataset.messageId = data.message_id; + } else { + showToast('Message sending failed'); + } + }).catch(error => { + showToast('Message sending failed'); + console.error('Error sending message:', error); + }); + } + }); + + document.querySelector('input[name="file"]').addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + const imagePreview = document.querySelector('.file-preview-image'); + const videoPreview = document.querySelector('.file-preview-video'); + const filenamePreview = document.querySelector('.file-preview-filename'); + reader.onload = function(e) { + document.querySelector('.file-preview').style.display = 'block'; + if (file.type.startsWith('image/')) { + imagePreview.style.display = 'block'; + videoPreview.style.display = 'none'; + filenamePreview.style.display = 'none'; + imagePreview.src = e.target.result; + } else if (file.type.startsWith('video/')) { + videoPreview.style.display = 'block'; + imagePreview.style.display = 'none'; + filenamePreview.style.display = 'none'; + videoPreview.querySelector('source').src = e.target.result; + videoPreview.load(); + } else { + filenamePreview.style.display = 'block'; + imagePreview.style.display = 'none'; + videoPreview.style.display = 'none'; + filenamePreview.textContent = file.name; + } + }; + reader.readAsDataURL(file); + } + }); + + document.querySelector('.remove-file').addEventListener('click', function() { + const fileInput = document.querySelector('input[name="file"]'); + fileInput.value = ''; + const filePreview = document.querySelector('.file-preview'); + filePreview.style.display = 'none'; + filePreview.querySelector('img').style.display = 'none'; + filePreview.querySelector('img').src = ''; + filePreview.querySelector('video').style.display = 'none'; + filePreview.querySelector('video').src = ''; + filePreview.querySelector('.file-preview-filename').style.display = 'none'; + filePreview.querySelector('.file-preview-filename').textContent = ''; + }); + } + + 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'; + }); + + // Custom Context Menu Functionality + document.addEventListener('contextmenu', function(event) { + const messageElement = event.target.closest('.message'); + if (messageElement) { + event.preventDefault(); + currentMessageId = messageElement.dataset.messageId; + currentMessageText = messageElement.dataset.messageText; + + contextMenu.style.top = `${event.clientY}px`; + contextMenu.style.left = `${event.clientX}px`; + contextMenu.style.display = 'block'; + } else { + contextMenu.style.display = 'none'; + } + }); + + document.getElementById('copy-text').addEventListener('click', function() { + if (currentMessageText) { + navigator.clipboard.writeText(currentMessageText).then(() => { + showToast('Text copied to clipboard'); + }); + } + contextMenu.style.display = 'none'; + }); + + document.getElementById('copy-id').addEventListener('click', function() { + if (currentMessageId) { + navigator.clipboard.writeText(currentMessageId).then(() => { + showToast('Message ID copied to clipboard'); + }); + } + contextMenu.style.display = 'none'; + }); + + contextMenu.addEventListener('contextmenu', function(event) { + event.preventDefault(); + }); + + document.addEventListener('click', function() { + contextMenu.style.display = 'none'; + }); + + // Handle user online/offline status + socket.on('user_online', function(data) { + const statusIndicator = document.getElementById(`status-${data.username}`); + if (statusIndicator) { + statusIndicator.classList.remove('offline'); + statusIndicator.classList.add('online'); + } + }); + + socket.on('user_offline', function(data) { + const statusIndicator = document.getElementById(`status-${data.username}`); + if (statusIndicator) { + statusIndicator.classList.remove('online'); + statusIndicator.classList.add('offline'); + } + }); +}); diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..f410fb5 --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,85 @@ +document.addEventListener('DOMContentLoaded', (event) => { + const socket = io(); + + socket.on('user_online', function(data) { + const username = data.username; + const friendElement = document.querySelector(`[data-username="${username}"]`); + if (friendElement) { + friendElement.querySelector('.status-indicator').classList.remove('offline'); + friendElement.querySelector('.status-indicator').classList.add('online'); + } + }); + + socket.on('user_offline', function(data) { + const username = data.username; + const friendElement = document.querySelector(`[data-username="${username}"]`); + if (friendElement) { + friendElement.querySelector('.status-indicator').classList.remove('online'); + friendElement.querySelector('.status-indicator').classList.add('offline'); + } + }); + + 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(); + + 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') { + 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') { + this.closest('.friend-request').remove(); + checkFriendRequests(); + } + }); + }); + }); + + const addFriendForm = document.getElementById('add-friend-form'); + if (addFriendForm) { + addFriendForm.addEventListener('submit', function(event) { + event.preventDefault(); + const formData = new FormData(addFriendForm); + fetch('/add_friend', { + method: 'POST', + body: formData + }).then(response => response.json()) + .then(data => { + if (data.status === 'Friend request sent') { + alert('Friend request sent'); + } else { + alert('Failed to send friend request'); + } + }); + }); + } +}); diff --git a/static/styles.css b/static/styles.css index 81e184b..2879b14 100644 --- a/static/styles.css +++ b/static/styles.css @@ -94,10 +94,16 @@ body { .send-message-form { display: flex; - align-items: center; + flex-direction: column; } -.send-message-form input[type="text"] { +.message-input-container { + display: flex; + align-items: center; + margin-top: 10px; +} + +.message-input-container input[type="text"] { flex: 1; padding: 10px; margin-right: 10px; @@ -105,7 +111,7 @@ body { border-radius: 5px; } -.send-message-form button { +.message-input-container button { padding: 10px; border: none; background-color: #7289da; @@ -114,18 +120,27 @@ body { cursor: pointer; } -.image-preview { +.add-file-btn { + margin-right: 10px; +} + +.file-preview { position: relative; display: inline-block; + margin-bottom: 10px; + max-width: 100%; } -.image-preview img { - max-width: 100px; +.file-preview img, +.file-preview video, +.file-preview .file-preview-filename { + max-width: 100%; max-height: 100px; display: block; + margin-bottom: 5px; } -.image-preview .remove-image { +.file-preview .remove-file { position: absolute; top: 0; right: 0; @@ -158,17 +173,100 @@ body { cursor: pointer; } -.online{ +.sidebar-tabs { + width: 100%; + height: 34px; + display: flex; + padding-left: 10px; + align-items: center; + border-radius: 5px; +} + +.sidebar-selected { + background: white; +} + +.sidebar-tabs-content { + font-size: 1.5rem; +} + +.sidebar-tabs-content > a { + color: black; +} + +/* Toast */ +.toast { + display: none; + position: fixed; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: #fff; + padding: 10px; + border-radius: 5px; + z-index: 1000; +} + +.toast.show { + display: block; + animation: fadeInOut 3s; +} + +@keyframes fadeInOut { + 0% { opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; } +} + +/* Right click menu */ +.custom-context-menu { + display: none; + position: absolute; + z-index: 1000; + width: 150px; + background-color: #fff; + border: 1px solid #ccc; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); +} + +.custom-context-menu ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.custom-context-menu ul li { + padding: 10px; + cursor: pointer; +} + +.custom-context-menu ul li:hover { + background-color: #eee; +} + +/* Status indicator */ +/* li { + list-style-type: none; +} */ + +ul#friends-list { + padding: 0; +} + +.status-indicator { margin-right: 10px; + display: inline-block; height: 10px; width: 10px; - background-color: #23a55a; border-radius: 50%; } -.offline{ - margin-right: 10px; - height: 10px; - width: 10px; + +.online { + background-color: #23a55a; +} + +.offline { background-color: #80848e; - border-radius: 50%; -} \ No newline at end of file +} diff --git a/templates/chat.html b/templates/chat.html index 344df7a..676522e 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -4,205 +4,20 @@ Chat + - + + + {% include 'sidebar.html' %} -

Chatting with {{ friend_username }}

@@ -211,27 +26,38 @@
- - - - - - -
diff --git a/templates/dashboard.html b/templates/dashboard.html index 128e05d..2799cd1 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -5,89 +5,12 @@ Dashboard - + + {% include 'sidebar.html' %}
-

Friends

- -

Friend Requests

    diff --git a/templates/login.html b/templates/login.html index 10a10c6..2f15760 100644 --- a/templates/login.html +++ b/templates/login.html @@ -3,16 +3,44 @@ Login + + + -

    Login

    -
    - -
    - -
    - -
    - Register +
    +

    Login

    +
    +
    + + +
    +
    + + +
    + +
    +
    + diff --git a/templates/sidebar.html b/templates/sidebar.html index 81c55b5..70b1f67 100644 --- a/templates/sidebar.html +++ b/templates/sidebar.html @@ -3,13 +3,24 @@ Sidebar +