Added new login screen and MD
This commit is contained in:
parent
918d6a9800
commit
b5937c03e6
9 changed files with 697 additions and 327 deletions
98
app.py
98
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 flask_sqlalchemy import SQLAlchemy
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
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 = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = os.urandom(24)
|
app.config['SECRET_KEY'] = os.urandom(24)
|
||||||
|
@ -28,6 +35,24 @@ if not os.path.exists("secret.key"):
|
||||||
key = load_key()
|
key = load_key()
|
||||||
cipher = Fernet(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):
|
class User(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
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)
|
receiver = db.Column(db.String(150), nullable=False)
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
content_type = db.Column(db.String(20), nullable=False, default='text')
|
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):
|
class PendingMessage(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -48,7 +73,7 @@ class PendingMessage(db.Model):
|
||||||
receiver = db.Column(db.String(150), nullable=False)
|
receiver = db.Column(db.String(150), nullable=False)
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
content_type = db.Column(db.String(20), nullable=False, default='text')
|
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):
|
class Friend(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -67,8 +92,15 @@ class FriendRequest(db.Model):
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
if 'username' in session:
|
token = request.cookies.get('token')
|
||||||
return redirect(url_for('dashboard'))
|
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'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
@ -81,8 +113,10 @@ def register():
|
||||||
try:
|
try:
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f"New user registered: {username}")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Error registering user {username}: {e}")
|
||||||
return 'Username already exists!'
|
return 'Username already exists!'
|
||||||
return render_template('register.html')
|
return render_template('register.html')
|
||||||
|
|
||||||
|
@ -95,8 +129,14 @@ def login():
|
||||||
if user and check_password_hash(user.password, password):
|
if user and check_password_hash(user.password, password):
|
||||||
session['username'] = user.username
|
session['username'] = user.username
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
return redirect(url_for('dashboard'))
|
token = generate_token(user.id)
|
||||||
return 'Invalid credentials'
|
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')
|
return render_template('login.html')
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
|
@ -125,6 +165,7 @@ def dashboard():
|
||||||
except InvalidToken:
|
except InvalidToken:
|
||||||
decrypted_messages.append((msg.sender, "Invalid encrypted message", msg.timestamp))
|
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 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'))
|
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_users = User.query.filter(User.id.in_(friend_ids)).all()
|
||||||
|
|
||||||
friend_requests = FriendRequest.query.filter_by(receiver_id=user.id, status='pending').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)
|
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:
|
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('dashboard'))
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@ -152,24 +195,25 @@ def send_message(receiver):
|
||||||
try:
|
try:
|
||||||
content = request.form.get('content')
|
content = request.form.get('content')
|
||||||
timestamp = request.form.get('timestamp')
|
timestamp = request.form.get('timestamp')
|
||||||
file = request.files.get('image') or request.files.get('video')
|
file = request.files.get('file')
|
||||||
content_type = 'text'
|
content_type = 'text'
|
||||||
|
|
||||||
if file:
|
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))
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
content = filename
|
content = filename
|
||||||
content_type = 'file'
|
content_type = 'file'
|
||||||
|
|
||||||
if content or file:
|
if content:
|
||||||
# Encrypt text content only, do not encrypt file names
|
# Encrypt text content only, do not encrypt file names
|
||||||
encrypted_content = cipher.encrypt(content.encode()).decode() if content_type == 'text' else content
|
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
|
# Check if they are friends
|
||||||
user = User.query.filter_by(username=session['username']).first()
|
user = User.query.filter_by(username=session['username']).first()
|
||||||
receiver_user = User.query.filter_by(id=receiver).first()
|
receiver_user = User.query.filter_by(id=receiver).first()
|
||||||
if not receiver_user:
|
if not receiver_user:
|
||||||
|
logger.error(f"Message send failed: User not found {receiver}")
|
||||||
return jsonify({'error': 'User not found'}), 404
|
return jsonify({'error': 'User not found'}), 404
|
||||||
friend = Friend.query.filter_by(user_id=user.id, friend_id=receiver_user.id).first()
|
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'],
|
'sender': session['username'],
|
||||||
'content': decrypted_content,
|
'content': decrypted_content,
|
||||||
'content_type': content_type,
|
'content_type': content_type,
|
||||||
'timestamp': timestamp
|
'timestamp': timestamp,
|
||||||
|
'id': new_message.id
|
||||||
}, room=receiver_user.username)
|
}, 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:
|
else:
|
||||||
pending_message = PendingMessage(sender=session['username'], receiver=receiver_user.username, content=encrypted_content, content_type=content_type, timestamp=timestamp_dt)
|
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.add(pending_message)
|
||||||
db.session.commit()
|
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
|
return jsonify({'error': 'No content or file provided'}), 400
|
||||||
except Exception as e:
|
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': str(e)}), 500
|
||||||
return jsonify({'error': 'Unauthorized'}), 401
|
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")}
|
{'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
|
for msg in messages
|
||||||
]
|
]
|
||||||
|
logger.info(f"Messages retrieved for chat between {current_user.username} and {friend_user.username}")
|
||||||
return jsonify({'messages': decrypted_messages})
|
return jsonify({'messages': decrypted_messages})
|
||||||
|
logger.warning(f"Unauthorized message retrieval attempt by user {session.get('username', 'unknown')}")
|
||||||
return jsonify({'error': 'Unauthorized'}), 401
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
@app.route('/cdn/<filename>')
|
@app.route('/cdn/<filename>')
|
||||||
|
@ -233,7 +284,9 @@ def add_friend():
|
||||||
'sender': session['username'],
|
'sender': session['username'],
|
||||||
'receiver': friend_username
|
'receiver': friend_username
|
||||||
}, room=friend_username)
|
}, room=friend_username)
|
||||||
|
logger.info(f"Friend request sent from {session['username']} to {friend_username}")
|
||||||
return redirect(url_for('dashboard'))
|
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 'Friend not found or cannot add yourself as a friend'
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@ -266,8 +319,10 @@ def accept_friend(request_id):
|
||||||
'sender': friend_request.sender.username,
|
'sender': friend_request.sender.username,
|
||||||
'receiver': friend_request.receiver.username
|
'receiver': friend_request.receiver.username
|
||||||
}, room=friend_request.sender.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'))
|
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 'Friend request not found'
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@ -283,8 +338,10 @@ def reject_friend(request_id):
|
||||||
'sender': friend_request.sender.username,
|
'sender': friend_request.sender.username,
|
||||||
'receiver': friend_request.receiver.username
|
'receiver': friend_request.receiver.username
|
||||||
}, room=friend_request.sender.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'))
|
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 'Friend request not found'
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@ -304,8 +361,10 @@ def remove_friend(friend_id):
|
||||||
'sender': session['username'],
|
'sender': session['username'],
|
||||||
'receiver': friend.friend.username
|
'receiver': friend.friend.username
|
||||||
}, room=friend.friend.username)
|
}, room=friend.friend.username)
|
||||||
|
logger.info(f"Friend {friend.friend.username} removed by {session['username']}")
|
||||||
|
|
||||||
return redirect(url_for('dashboard'))
|
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 'Friend not found'
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@ -313,7 +372,10 @@ def remove_friend(friend_id):
|
||||||
def logout():
|
def logout():
|
||||||
session.pop('username', None)
|
session.pop('username', None)
|
||||||
session.pop('user_id', 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')
|
@socketio.on('connect')
|
||||||
def handle_connect():
|
def handle_connect():
|
||||||
|
@ -323,6 +385,7 @@ def handle_connect():
|
||||||
user.online = True
|
user.online = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
emit('user_online', {'username': user.username}, broadcast=True)
|
emit('user_online', {'username': user.username}, broadcast=True)
|
||||||
|
logger.info(f"User {session['username']} connected")
|
||||||
|
|
||||||
@socketio.on('disconnect')
|
@socketio.on('disconnect')
|
||||||
def handle_disconnect():
|
def handle_disconnect():
|
||||||
|
@ -332,18 +395,21 @@ def handle_disconnect():
|
||||||
user.online = False
|
user.online = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
emit('user_offline', {'username': user.username}, broadcast=True)
|
emit('user_offline', {'username': user.username}, broadcast=True)
|
||||||
|
logger.info(f"User {session['username']} disconnected")
|
||||||
|
|
||||||
@socketio.on('join')
|
@socketio.on('join')
|
||||||
def handle_join(data):
|
def handle_join(data):
|
||||||
username = data['username']
|
username = data['username']
|
||||||
join_room(username)
|
join_room(username)
|
||||||
|
logger.info(f"User {username} joined room {username}")
|
||||||
|
|
||||||
@socketio.on('leave')
|
@socketio.on('leave')
|
||||||
def handle_leave(data):
|
def handle_leave(data):
|
||||||
username = data['username']
|
username = data['username']
|
||||||
leave_room(username)
|
leave_room(username)
|
||||||
|
logger.info(f"User {username} left room {username}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
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,
|
||||||
|
|
|
@ -5,3 +5,4 @@ cryptography==42.0.8
|
||||||
Werkzeug==3.0.3
|
Werkzeug==3.0.3
|
||||||
python-socketio==5.11.2
|
python-socketio==5.11.2
|
||||||
python-engineio==4.9.1
|
python-engineio==4.9.1
|
||||||
|
PyJWT==2.8.0
|
332
static/js/chat.js
Normal file
332
static/js/chat.js
Normal file
|
@ -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 = `<strong>${data.sender}</strong>:`;
|
||||||
|
messagesList.appendChild(usernameElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.content_type === 'text') {
|
||||||
|
newMessage.innerHTML = `${md.render(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 = `<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 = `<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 = `<a href="/cdn/${data.content}" target="_blank">Download File</a><div class="timestamp">${data.timestamp}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<strong>${msg.sender}</strong>:`;
|
||||||
|
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)}<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 = `<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 = `<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 = `<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="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 = `<strong>${username}</strong>:`;
|
||||||
|
messagesList.appendChild(usernameElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
newMessage.dataset.messageText = content;
|
||||||
|
newMessage.innerHTML = `${md.render(content)}<div class="timestamp">${timestamp}</div>`;
|
||||||
|
}
|
||||||
|
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 = `<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 = `<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 = `<a href="${URL.createObjectURL(file)}" 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('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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
85
static/js/dashboard.js
Normal file
85
static/js/dashboard.js
Normal file
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -94,10 +94,16 @@ body {
|
||||||
|
|
||||||
.send-message-form {
|
.send-message-form {
|
||||||
display: flex;
|
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;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
@ -105,7 +111,7 @@ body {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-message-form button {
|
.message-input-container button {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #7289da;
|
background-color: #7289da;
|
||||||
|
@ -114,18 +120,27 @@ body {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview {
|
.add-file-btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview img {
|
.file-preview img,
|
||||||
max-width: 100px;
|
.file-preview video,
|
||||||
|
.file-preview .file-preview-filename {
|
||||||
|
max-width: 100%;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
display: block;
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview .remove-image {
|
.file-preview .remove-file {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -158,17 +173,100 @@ body {
|
||||||
cursor: pointer;
|
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;
|
margin-right: 10px;
|
||||||
|
display: inline-block;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
background-color: #23a55a;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.offline{
|
|
||||||
margin-right: 10px;
|
.online {
|
||||||
height: 10px;
|
background-color: #23a55a;
|
||||||
width: 10px;
|
}
|
||||||
|
|
||||||
|
.offline {
|
||||||
background-color: #80848e;
|
background-color: #80848e;
|
||||||
border-radius: 50%;
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -4,205 +4,20 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Chat</title>
|
<title>Chat</title>
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://vjs.zencdn.net/7.11.4/video-js.css">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.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 src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
|
||||||
<script type="text/javascript">
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
|
||||||
const socket = io();
|
<script>
|
||||||
socket.emit('join', { username: "{{ username }}" });
|
var friendId = "{{ friend_id }}";
|
||||||
|
var friendUsername = "{{ friend_username }}";
|
||||||
const messagesList = document.getElementById('messages');
|
var username = "{{ username }}";
|
||||||
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>
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'sidebar.html' %}
|
{% include 'sidebar.html' %}
|
||||||
|
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<h3 id="chat-with">Chatting with {{ friend_username }}</h3>
|
<h3 id="chat-with">Chatting with {{ friend_username }}</h3>
|
||||||
|
@ -211,27 +26,38 @@
|
||||||
<!-- Messages will be displayed here -->
|
<!-- Messages will be displayed here -->
|
||||||
</div>
|
</div>
|
||||||
<form class="send-message-form" id="send-message-form" data-receiver="{{ friend_id }}" method="POST">
|
<form class="send-message-form" id="send-message-form" data-receiver="{{ friend_id }}" method="POST">
|
||||||
<input type="text" name="content" placeholder="Type a message">
|
<div class="file-preview" style="display: none;">
|
||||||
<input type="file" name="image" style="display: none;">
|
<img src="" alt="Image Preview" class="file-preview-image" style="display: none;">
|
||||||
<input type="file" name="video" style="display: none;">
|
<video controls class="file-preview-video video-js vjs-default-skin" 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">
|
<source src="" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
<button type="button" class="remove-video">X</button>
|
<div class="file-preview-filename" style="display: none;"></div>
|
||||||
|
<button type="button" class="remove-file">X</button>
|
||||||
|
</div>
|
||||||
|
<div class="message-input-container">
|
||||||
|
<input type="text" name="content" placeholder="Type a message">
|
||||||
|
<input type="file" name="file" style="display: none;" accept="image/*,video/*,application/pdf,application/msword">
|
||||||
|
<button type="button" class="add-file-btn" onclick="document.querySelector('input[name=\'file\']').click()">+</button>
|
||||||
|
<button type="submit" class="send-btn">Send</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Send</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="image-overlay" class="image-overlay">
|
<div id="image-overlay" class="image-overlay">
|
||||||
<img id="overlay-image" src="" alt="Enhanced Image">
|
<img id="overlay-image" src="" alt="Enhanced Image">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Context Menu -->
|
||||||
|
<div id="context-menu" class="custom-context-menu">
|
||||||
|
<ul>
|
||||||
|
<li id="copy-text">Copy Text</li>
|
||||||
|
<li id="copy-id">Copy Message ID</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div id="toast" class="toast">
|
||||||
|
<div id="toast-message"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,89 +5,12 @@
|
||||||
<title>Dashboard</title>
|
<title>Dashboard</title>
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
<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') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
<script>
|
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||||||
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();
|
|
||||||
|
|
||||||
// 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 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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'sidebar.html' %}
|
{% include 'sidebar.html' %}
|
||||||
<div class="content">
|
<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">
|
<div id="friend-requests-section">
|
||||||
<h4>Friend Requests</h4>
|
<h4>Friend Requests</h4>
|
||||||
<ul id="friend-requests">
|
<ul id="friend-requests">
|
||||||
|
|
|
@ -3,16 +3,44 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Login</title>
|
<title>Login</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.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Login</h2>
|
<div class="container">
|
||||||
<form method="POST">
|
<h2>Login</h2>
|
||||||
<label for="username">Username:</label>
|
<form id="login-form" method="POST">
|
||||||
<input type="text" id="username" name="username" required><br>
|
<div class="form-group">
|
||||||
<label for="password">Password:</label>
|
<label for="username">Username</label>
|
||||||
<input type="password" id="password" name="password" required><br>
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
<button type="submit">Login</button>
|
</div>
|
||||||
</form>
|
<div class="form-group">
|
||||||
<a href="{{ url_for('register') }}">Register</a>
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('login-form').addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
axios.post('/login', formData)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
} else {
|
||||||
|
alert('Invalid credentials. Please try again.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error logging in:', error);
|
||||||
|
alert('An error occurred. Please try again.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,13 +3,24 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Sidebar</title>
|
<title>Sidebar</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<h3>Welcome, {{ username }}</h3>
|
<h3>Welcome, {{ username }}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/dashboard">Dashboard</a></li>
|
<div class="sidebar-tabs sidebar-selected">
|
||||||
<li><a href="/logout">Logout</a></li>
|
<div class="sidebar-tabs-content"><a href="/dashboard">Friends</a></div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
<h4>Direct Messages</h4>
|
||||||
|
<ul id="friends-list">
|
||||||
|
{% for friend in friends %}
|
||||||
|
<li data-username="{{ friend.username }}">
|
||||||
|
<span class="status-indicator {{ 'online' if friend.online else 'offline' }}" id="status-{{ friend.username }}"></span>
|
||||||
|
<a href="/chat/{{ friend.id }}">{{ friend.username }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue